Spaces:
Running on Zero
Running on Zero
Upload 6 files
#3
by specimba - opened
- nexus_hf_mcp_bridge/README.md +83 -0
- nexus_hf_mcp_bridge/__init__.py +29 -0
- nexus_hf_mcp_bridge/hf_oauth.py +121 -0
- nexus_hf_mcp_bridge/hf_repo_tools.py +104 -0
- nexus_hf_mcp_bridge/mcp_bridge.py +139 -0
- nexus_hf_mcp_bridge/token_manager.py +108 -0
nexus_hf_mcp_bridge/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# NEXUS HF MCP Bridge
|
| 2 |
+
|
| 3 |
+
Clean, sovereign Hugging Face integration for the **NEXUS Visual Weaver** Space.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **OAuth Authentication**: Device Code + PKCE flow using the "X - GROK integration for HF" app
|
| 8 |
+
- **Token Management**: Space-friendly token storage with environment variable support
|
| 9 |
+
- **Repository Operations**: Upload, create/update files, list repos using `huggingface_hub`
|
| 10 |
+
- **MCP Tool Exposure**: Ready to be used as MCP tools inside Gradio Spaces (`mcp_server=True`)
|
| 11 |
+
|
| 12 |
+
## Folder Structure
|
| 13 |
+
|
| 14 |
+
```
|
| 15 |
+
nexus_hf_mcp_bridge/
|
| 16 |
+
├── __init__.py
|
| 17 |
+
├── hf_oauth.py # OAuth Device Code + PKCE
|
| 18 |
+
├── token_manager.py # Token storage & refresh
|
| 19 |
+
├── hf_repo_tools.py # huggingface_hub operations
|
| 20 |
+
├── mcp_bridge.py # Main bridge + MCP tools
|
| 21 |
+
└── README.md
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
## Quick Start (Inside the Space)
|
| 25 |
+
|
| 26 |
+
```python
|
| 27 |
+
from nexus_hf_mcp_bridge import nexus_hf_bridge
|
| 28 |
+
|
| 29 |
+
# 1. Authenticate (one-time)
|
| 30 |
+
result = nexus_hf_bridge.tool_authenticate()
|
| 31 |
+
|
| 32 |
+
# 2. Use tools
|
| 33 |
+
nexus_hf_bridge.tool_create_or_update_file(
|
| 34 |
+
repo_id="your-username/your-repo",
|
| 35 |
+
path_in_repo="example.txt",
|
| 36 |
+
content="Hello from NEXUS HF MCP Bridge!"
|
| 37 |
+
)
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
## Integration into `app.py`
|
| 41 |
+
|
| 42 |
+
```python
|
| 43 |
+
from nexus_hf_mcp_bridge import nexus_hf_bridge
|
| 44 |
+
import gradio as gr
|
| 45 |
+
|
| 46 |
+
with gr.Blocks() as demo:
|
| 47 |
+
gr.Markdown("# NEXUS Visual Weaver - HF Bridge")
|
| 48 |
+
|
| 49 |
+
with gr.Tab("HF Tools"):
|
| 50 |
+
repo = gr.Textbox(label="Repository ID")
|
| 51 |
+
path = gr.Textbox(label="Path in Repo")
|
| 52 |
+
content = gr.Textbox(label="Content", lines=8)
|
| 53 |
+
btn = gr.Button("Create / Update File")
|
| 54 |
+
out = gr.JSON()
|
| 55 |
+
|
| 56 |
+
btn.click(
|
| 57 |
+
nexus_hf_bridge.tool_create_or_update_file,
|
| 58 |
+
inputs=[repo, path, content],
|
| 59 |
+
outputs=out
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
demo.launch(mcp_server=True)
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
## Authentication
|
| 66 |
+
|
| 67 |
+
The bridge supports two modes:
|
| 68 |
+
|
| 69 |
+
1. **Interactive** (Device Code flow) — Good for development
|
| 70 |
+
2. **Environment Variable** (recommended for Spaces):
|
| 71 |
+
```bash
|
| 72 |
+
HF_OAUTH_ACCESS_TOKEN=hf_xxxxxxxxxxxxxxxx
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## Security Notes
|
| 76 |
+
|
| 77 |
+
- Never commit tokens
|
| 78 |
+
- Use Space Secrets for production
|
| 79 |
+
- Token refresh logic is prepared for future extension
|
| 80 |
+
|
| 81 |
+
## License
|
| 82 |
+
|
| 83 |
+
Internal NEXUS OS Project
|
nexus_hf_mcp_bridge/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
NEXUS HF MCP Bridge
|
| 3 |
+
|
| 4 |
+
A clean, sovereign Hugging Face integration layer for the NEXUS Visual Weaver Space.
|
| 5 |
+
|
| 6 |
+
Provides:
|
| 7 |
+
- OAuth Device Code + PKCE authentication
|
| 8 |
+
- Token management (Space-friendly)
|
| 9 |
+
- High-level huggingface_hub operations
|
| 10 |
+
- MCP tool exposure for Gradio Spaces
|
| 11 |
+
|
| 12 |
+
Author: NEXUS OS Team
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from .hf_oauth import HFOAuthClient, HFOAuthToken, authenticate_hf
|
| 16 |
+
from .token_manager import HFTokenManager
|
| 17 |
+
from .hf_repo_tools import HFRepoTools
|
| 18 |
+
from .mcp_bridge import NEXUS_HF_MCP_Bridge, nexus_hf_bridge
|
| 19 |
+
|
| 20 |
+
__version__ = "0.1.0"
|
| 21 |
+
__all__ = [
|
| 22 |
+
"HFOAuthClient",
|
| 23 |
+
"HFOAuthToken",
|
| 24 |
+
"authenticate_hf",
|
| 25 |
+
"HFTokenManager",
|
| 26 |
+
"HFRepoTools",
|
| 27 |
+
"NEXUS_HF_MCP_Bridge",
|
| 28 |
+
"nexus_hf_bridge",
|
| 29 |
+
]
|
nexus_hf_mcp_bridge/hf_oauth.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
hf_oauth.py
|
| 3 |
+
Custom HF MCP Bridge - OAuth Layer
|
| 4 |
+
Handles Device Code + PKCE flow for the "X - GROK integration for HF" OAuth app.
|
| 5 |
+
|
| 6 |
+
This module is designed to run inside the NEXUS Visual Weaver Space
|
| 7 |
+
(which has full internet access).
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import requests
|
| 11 |
+
import time
|
| 12 |
+
from typing import Optional, Dict, Any
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
|
| 15 |
+
CLIENT_ID = "6b41a3bf-689f-4b48-bbce-b39adc7b515d"
|
| 16 |
+
DEVICE_CODE_URL = "https://huggingface.co/oauth/device"
|
| 17 |
+
TOKEN_URL = "https://huggingface.co/oauth/token"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class HFOAuthToken:
|
| 22 |
+
access_token: str
|
| 23 |
+
token_type: str = "bearer"
|
| 24 |
+
expires_in: int = 0
|
| 25 |
+
refresh_token: Optional[str] = None
|
| 26 |
+
scope: Optional[str] = None
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class HFOAuthClient:
|
| 30 |
+
"""
|
| 31 |
+
Handles OAuth authentication for Hugging Face using Device Code flow.
|
| 32 |
+
Suitable for CLI agents and Spaces.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(self, client_id: str = CLIENT_ID):
|
| 36 |
+
self.client_id = client_id
|
| 37 |
+
self._current_token: Optional[HFOAuthToken] = None
|
| 38 |
+
|
| 39 |
+
def start_device_flow(self) -> Dict[str, Any]:
|
| 40 |
+
"""Step 1: Request device code and user verification URL."""
|
| 41 |
+
response = requests.post(
|
| 42 |
+
DEVICE_CODE_URL,
|
| 43 |
+
data={"client_id": self.client_id},
|
| 44 |
+
timeout=10
|
| 45 |
+
)
|
| 46 |
+
response.raise_for_status()
|
| 47 |
+
return response.json()
|
| 48 |
+
|
| 49 |
+
def poll_for_token(self, device_code: str, interval: int = 5, max_wait: int = 300) -> Optional[HFOAuthToken]:
|
| 50 |
+
"""
|
| 51 |
+
Step 2: Poll the token endpoint until the user authorizes.
|
| 52 |
+
"""
|
| 53 |
+
start_time = time.time()
|
| 54 |
+
while time.time() - start_time < max_wait:
|
| 55 |
+
response = requests.post(
|
| 56 |
+
TOKEN_URL,
|
| 57 |
+
data={
|
| 58 |
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
| 59 |
+
"device_code": device_code,
|
| 60 |
+
"client_id": self.client_id,
|
| 61 |
+
},
|
| 62 |
+
timeout=10
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
if response.status_code == 200:
|
| 66 |
+
data = response.json()
|
| 67 |
+
token = HFOAuthToken(
|
| 68 |
+
access_token=data["access_token"],
|
| 69 |
+
token_type=data.get("token_type", "bearer"),
|
| 70 |
+
expires_in=data.get("expires_in", 0),
|
| 71 |
+
refresh_token=data.get("refresh_token"),
|
| 72 |
+
scope=data.get("scope"),
|
| 73 |
+
)
|
| 74 |
+
self._current_token = token
|
| 75 |
+
return token
|
| 76 |
+
|
| 77 |
+
elif response.status_code == 400:
|
| 78 |
+
error = response.json().get("error")
|
| 79 |
+
if error == "authorization_pending":
|
| 80 |
+
time.sleep(interval)
|
| 81 |
+
continue
|
| 82 |
+
elif error == "slow_down":
|
| 83 |
+
time.sleep(interval + 5)
|
| 84 |
+
continue
|
| 85 |
+
else:
|
| 86 |
+
print(f"[OAuth] Error: {response.json()}")
|
| 87 |
+
return None
|
| 88 |
+
else:
|
| 89 |
+
print(f"[OAuth] Unexpected status: {response.status_code}")
|
| 90 |
+
return None
|
| 91 |
+
|
| 92 |
+
print("[OAuth] Timeout waiting for user authorization.")
|
| 93 |
+
return None
|
| 94 |
+
|
| 95 |
+
def get_token(self) -> Optional[HFOAuthToken]:
|
| 96 |
+
"""Return the current valid token (if any)."""
|
| 97 |
+
return self._current_token
|
| 98 |
+
|
| 99 |
+
def is_authenticated(self) -> bool:
|
| 100 |
+
return self._current_token is not None and bool(self._current_token.access_token)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# Convenience function for quick usage
|
| 104 |
+
def authenticate_hf() -> Optional[HFOAuthToken]:
|
| 105 |
+
"""
|
| 106 |
+
Full Device Code flow in one call.
|
| 107 |
+
Returns HFOAuthToken or None.
|
| 108 |
+
"""
|
| 109 |
+
client = HFOAuthClient()
|
| 110 |
+
device_data = client.start_device_flow()
|
| 111 |
+
|
| 112 |
+
print("\n=== Hugging Face OAuth ===")
|
| 113 |
+
print(f"Open this URL: {device_data['verification_uri']}")
|
| 114 |
+
print(f"Enter code: {device_data['user_code']}\n")
|
| 115 |
+
|
| 116 |
+
input("Press Enter after you have authorized in the browser...")
|
| 117 |
+
|
| 118 |
+
token = client.poll_for_token(device_data["device_code"])
|
| 119 |
+
if token:
|
| 120 |
+
print("[+] Successfully authenticated with Hugging Face!")
|
| 121 |
+
return token
|
nexus_hf_mcp_bridge/hf_repo_tools.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
hf_repo_tools.py
|
| 3 |
+
Custom HF MCP Bridge - Repository Operations
|
| 4 |
+
|
| 5 |
+
Core Hugging Face Hub operations (push, update, list, etc.)
|
| 6 |
+
using huggingface_hub library + our OAuth token.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from typing import Optional, List, Dict, Any
|
| 10 |
+
from huggingface_hub import HfApi, upload_file, create_repo, list_repo_files
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class HFRepoTools:
|
| 15 |
+
"""
|
| 16 |
+
High-level wrapper around huggingface_hub for common operations.
|
| 17 |
+
Uses token from HFTokenManager.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def __init__(self, token: str):
|
| 21 |
+
self.token = token
|
| 22 |
+
self.api = HfApi(token=token)
|
| 23 |
+
|
| 24 |
+
def upload_file_to_repo(
|
| 25 |
+
self,
|
| 26 |
+
local_path: str,
|
| 27 |
+
repo_id: str,
|
| 28 |
+
path_in_repo: str,
|
| 29 |
+
repo_type: str = "model",
|
| 30 |
+
commit_message: Optional[str] = None
|
| 31 |
+
) -> bool:
|
| 32 |
+
"""Upload a single file to a Hugging Face repo."""
|
| 33 |
+
try:
|
| 34 |
+
upload_file(
|
| 35 |
+
path_or_fileobj=local_path,
|
| 36 |
+
path_in_repo=path_in_repo,
|
| 37 |
+
repo_id=repo_id,
|
| 38 |
+
repo_type=repo_type,
|
| 39 |
+
token=self.token,
|
| 40 |
+
commit_message=commit_message or f"Upload {path_in_repo}"
|
| 41 |
+
)
|
| 42 |
+
return True
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"[HFRepoTools] Upload failed: {e}")
|
| 45 |
+
return False
|
| 46 |
+
|
| 47 |
+
def create_or_update_file(
|
| 48 |
+
self,
|
| 49 |
+
repo_id: str,
|
| 50 |
+
path_in_repo: str,
|
| 51 |
+
content: str,
|
| 52 |
+
commit_message: str = "Update file via NEXUS MCP Bridge"
|
| 53 |
+
) -> bool:
|
| 54 |
+
"""Create or update a text file in a repo."""
|
| 55 |
+
try:
|
| 56 |
+
self.api.upload_file(
|
| 57 |
+
path_or_fileobj=content.encode("utf-8"),
|
| 58 |
+
path_in_repo=path_in_repo,
|
| 59 |
+
repo_id=repo_id,
|
| 60 |
+
token=self.token,
|
| 61 |
+
commit_message=commit_message
|
| 62 |
+
)
|
| 63 |
+
return True
|
| 64 |
+
except Exception as e:
|
| 65 |
+
print(f"[HFRepoTools] create_or_update_file failed: {e}")
|
| 66 |
+
return False
|
| 67 |
+
|
| 68 |
+
def list_files(self, repo_id: str, revision: str = "main") -> List[str]:
|
| 69 |
+
"""List files in a repository."""
|
| 70 |
+
try:
|
| 71 |
+
files = list_repo_files(repo_id=repo_id, revision=revision, token=self.token)
|
| 72 |
+
return files
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print(f"[HFRepoTools] list_files failed: {e}")
|
| 75 |
+
return []
|
| 76 |
+
|
| 77 |
+
def repo_exists(self, repo_id: str) -> bool:
|
| 78 |
+
"""Check if a repository exists."""
|
| 79 |
+
try:
|
| 80 |
+
self.api.repo_info(repo_id=repo_id, token=self.token)
|
| 81 |
+
return True
|
| 82 |
+
except:
|
| 83 |
+
return False
|
| 84 |
+
|
| 85 |
+
def create_repo_if_not_exists(
|
| 86 |
+
self,
|
| 87 |
+
repo_id: str,
|
| 88 |
+
repo_type: str = "model",
|
| 89 |
+
private: bool = False
|
| 90 |
+
) -> bool:
|
| 91 |
+
"""Create a repository if it doesn't exist."""
|
| 92 |
+
if self.repo_exists(repo_id):
|
| 93 |
+
return True
|
| 94 |
+
try:
|
| 95 |
+
create_repo(
|
| 96 |
+
repo_id=repo_id,
|
| 97 |
+
repo_type=repo_type,
|
| 98 |
+
private=private,
|
| 99 |
+
token=self.token
|
| 100 |
+
)
|
| 101 |
+
return True
|
| 102 |
+
except Exception as e:
|
| 103 |
+
print(f"[HFRepoTools] Failed to create repo: {e}")
|
| 104 |
+
return False
|
nexus_hf_mcp_bridge/mcp_bridge.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
mcp_bridge.py
|
| 3 |
+
Custom HF MCP Bridge - Main Integration Layer
|
| 4 |
+
|
| 5 |
+
Exposes Hugging Face operations as MCP tools for Gradio Spaces.
|
| 6 |
+
This is the main entry point for the NEXUS Visual Weaver Space.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from typing import Dict, Any, List, Optional
|
| 10 |
+
from .hf_oauth import HFOAuthClient, authenticate_hf
|
| 11 |
+
from .token_manager import HFTokenManager
|
| 12 |
+
from .hf_repo_tools import HFRepoTools
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class NEXUS_HF_MCP_Bridge:
|
| 16 |
+
"""
|
| 17 |
+
Main bridge class that exposes HF capabilities as MCP tools.
|
| 18 |
+
Designed to be used inside a Gradio Space with mcp_server=True.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(self):
|
| 22 |
+
self.token_manager = HFTokenManager()
|
| 23 |
+
self.oauth_client = HFOAuthClient()
|
| 24 |
+
self.repo_tools: Optional[HFRepoTools] = None
|
| 25 |
+
self._authenticated = False
|
| 26 |
+
|
| 27 |
+
def authenticate(self) -> Dict[str, Any]:
|
| 28 |
+
"""Start OAuth Device Code flow and store token."""
|
| 29 |
+
token = authenticate_hf()
|
| 30 |
+
if token:
|
| 31 |
+
self.token_manager.save_token(token)
|
| 32 |
+
self.repo_tools = HFRepoTools(token.access_token)
|
| 33 |
+
self._authenticated = True
|
| 34 |
+
return {"status": "success", "message": "Authenticated successfully"}
|
| 35 |
+
return {"status": "error", "message": "Authentication failed"}
|
| 36 |
+
|
| 37 |
+
def ensure_ready(self) -> bool:
|
| 38 |
+
"""Make sure we have a valid token and repo_tools ready."""
|
| 39 |
+
if self._authenticated and self.repo_tools:
|
| 40 |
+
return True
|
| 41 |
+
|
| 42 |
+
token_str = self.token_manager.get_valid_token()
|
| 43 |
+
if token_str:
|
| 44 |
+
self.repo_tools = HFRepoTools(token_str)
|
| 45 |
+
self._authenticated = True
|
| 46 |
+
return True
|
| 47 |
+
|
| 48 |
+
return False
|
| 49 |
+
|
| 50 |
+
# ===================== MCP TOOLS =====================
|
| 51 |
+
|
| 52 |
+
def tool_authenticate(self) -> Dict[str, Any]:
|
| 53 |
+
"""MCP Tool: Start Hugging Face OAuth authentication."""
|
| 54 |
+
return self.authenticate()
|
| 55 |
+
|
| 56 |
+
def tool_upload_file(
|
| 57 |
+
self,
|
| 58 |
+
local_path: str,
|
| 59 |
+
repo_id: str,
|
| 60 |
+
path_in_repo: str,
|
| 61 |
+
commit_message: Optional[str] = None
|
| 62 |
+
) -> Dict[str, Any]:
|
| 63 |
+
"""MCP Tool: Upload a file to a Hugging Face repository."""
|
| 64 |
+
if not self.ensure_ready():
|
| 65 |
+
return {"status": "error", "message": "Not authenticated. Call authenticate first."}
|
| 66 |
+
|
| 67 |
+
success = self.repo_tools.upload_file_to_repo(
|
| 68 |
+
local_path=local_path,
|
| 69 |
+
repo_id=repo_id,
|
| 70 |
+
path_in_repo=path_in_repo,
|
| 71 |
+
commit_message=commit_message
|
| 72 |
+
)
|
| 73 |
+
return {"status": "success" if success else "error"}
|
| 74 |
+
|
| 75 |
+
def tool_create_or_update_file(
|
| 76 |
+
self,
|
| 77 |
+
repo_id: str,
|
| 78 |
+
path_in_repo: str,
|
| 79 |
+
content: str,
|
| 80 |
+
commit_message: str = "Update via NEXUS HF MCP Bridge"
|
| 81 |
+
) -> Dict[str, Any]:
|
| 82 |
+
"""MCP Tool: Create or update a text file in a repo."""
|
| 83 |
+
if not self.ensure_ready():
|
| 84 |
+
return {"status": "error", "message": "Not authenticated."}
|
| 85 |
+
|
| 86 |
+
success = self.repo_tools.create_or_update_file(
|
| 87 |
+
repo_id=repo_id,
|
| 88 |
+
path_in_repo=path_in_repo,
|
| 89 |
+
content=content,
|
| 90 |
+
commit_message=commit_message
|
| 91 |
+
)
|
| 92 |
+
return {"status": "success" if success else "error"}
|
| 93 |
+
|
| 94 |
+
def tool_list_repo_files(self, repo_id: str) -> Dict[str, Any]:
|
| 95 |
+
"""MCP Tool: List files in a repository."""
|
| 96 |
+
if not self.ensure_ready():
|
| 97 |
+
return {"status": "error", "message": "Not authenticated."}
|
| 98 |
+
|
| 99 |
+
files = self.repo_tools.list_files(repo_id=repo_id)
|
| 100 |
+
return {"status": "success", "files": files}
|
| 101 |
+
|
| 102 |
+
def tool_repo_exists(self, repo_id: str) -> Dict[str, Any]:
|
| 103 |
+
"""MCP Tool: Check if a repository exists."""
|
| 104 |
+
if not self.ensure_ready():
|
| 105 |
+
return {"status": "error", "message": "Not authenticated."}
|
| 106 |
+
|
| 107 |
+
exists = self.repo_tools.repo_exists(repo_id=repo_id)
|
| 108 |
+
return {"status": "success", "exists": exists}
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# Global bridge instance (for use in Gradio)
|
| 112 |
+
nexus_hf_bridge = NEXUS_HF_MCP_Bridge()
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# Example of how to expose in Gradio app.py:
|
| 116 |
+
"""
|
| 117 |
+
from mcp_bridge import nexus_hf_bridge
|
| 118 |
+
|
| 119 |
+
with gr.Blocks() as demo:
|
| 120 |
+
gr.Markdown("# NEXUS Visual Weaver - HF MCP Bridge")
|
| 121 |
+
|
| 122 |
+
with gr.Tab("HF Authentication"):
|
| 123 |
+
auth_btn = gr.Button("Authenticate with Hugging Face")
|
| 124 |
+
auth_output = gr.JSON()
|
| 125 |
+
auth_btn.click(nexus_hf_bridge.tool_authenticate, outputs=auth_output)
|
| 126 |
+
|
| 127 |
+
with gr.Tab("Repository Tools"):
|
| 128 |
+
repo_id = gr.Textbox(label="Repository ID (e.g. username/my-model)")
|
| 129 |
+
file_path = gr.Textbox(label="Path in repo")
|
| 130 |
+
content = gr.Textbox(label="Content", lines=10)
|
| 131 |
+
upload_btn = gr.Button("Create / Update File")
|
| 132 |
+
result = gr.JSON()
|
| 133 |
+
|
| 134 |
+
upload_btn.click(
|
| 135 |
+
nexus_hf_bridge.tool_create_or_update_file,
|
| 136 |
+
inputs=[repo_id, file_path, content],
|
| 137 |
+
outputs=result
|
| 138 |
+
)
|
| 139 |
+
"""
|
nexus_hf_mcp_bridge/token_manager.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
token_manager.py
|
| 3 |
+
Custom HF MCP Bridge - Token Management Layer
|
| 4 |
+
|
| 5 |
+
Handles secure storage and refresh of Hugging Face OAuth tokens
|
| 6 |
+
in a Space-friendly way (environment variables + optional file fallback).
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import time
|
| 12 |
+
from typing import Optional
|
| 13 |
+
from datetime import datetime, timedelta
|
| 14 |
+
from .hf_oauth import HFOAuthToken, HFOAuthClient
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class HFTokenManager:
|
| 18 |
+
"""
|
| 19 |
+
Manages Hugging Face OAuth tokens with refresh support.
|
| 20 |
+
Designed to work well inside Hugging Face Spaces.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(self, token_file: str = "/tmp/hf_oauth_token.json"):
|
| 24 |
+
self.token_file = token_file
|
| 25 |
+
self._token: Optional[HFOAuthToken] = None
|
| 26 |
+
self._client = HFOAuthClient()
|
| 27 |
+
|
| 28 |
+
def load_token(self) -> Optional[HFOAuthToken]:
|
| 29 |
+
"""Load token from environment or file."""
|
| 30 |
+
# Priority 1: Environment variable (recommended for Spaces)
|
| 31 |
+
env_token = os.environ.get("HF_OAUTH_ACCESS_TOKEN")
|
| 32 |
+
if env_token:
|
| 33 |
+
self._token = HFOAuthToken(access_token=env_token)
|
| 34 |
+
return self._token
|
| 35 |
+
|
| 36 |
+
# Priority 2: Token file
|
| 37 |
+
if os.path.exists(self.token_file):
|
| 38 |
+
try:
|
| 39 |
+
with open(self.token_file, "r") as f:
|
| 40 |
+
data = json.load(f)
|
| 41 |
+
self._token = HFOAuthToken(**data)
|
| 42 |
+
return self._token
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"[TokenManager] Failed to load token file: {e}")
|
| 45 |
+
|
| 46 |
+
return None
|
| 47 |
+
|
| 48 |
+
def save_token(self, token: HFOAuthToken) -> bool:
|
| 49 |
+
"""Save token to file (and optionally env for current process)."""
|
| 50 |
+
self._token = token
|
| 51 |
+
try:
|
| 52 |
+
with open(self.token_file, "w") as f:
|
| 53 |
+
json.dump({
|
| 54 |
+
"access_token": token.access_token,
|
| 55 |
+
"token_type": token.token_type,
|
| 56 |
+
"expires_in": token.expires_in,
|
| 57 |
+
"refresh_token": token.refresh_token,
|
| 58 |
+
"scope": token.scope,
|
| 59 |
+
}, f, indent=2)
|
| 60 |
+
return True
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"[TokenManager] Failed to save token: {e}")
|
| 63 |
+
return False
|
| 64 |
+
|
| 65 |
+
def get_valid_token(self) -> Optional[str]:
|
| 66 |
+
"""
|
| 67 |
+
Return a valid access token.
|
| 68 |
+
If expired and refresh_token exists, attempt refresh.
|
| 69 |
+
"""
|
| 70 |
+
if not self._token:
|
| 71 |
+
self.load_token()
|
| 72 |
+
|
| 73 |
+
if not self._token:
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
# For simplicity in Phase 1, we assume the token is still valid
|
| 77 |
+
# In production we would check expiry and refresh here.
|
| 78 |
+
return self._token.access_token
|
| 79 |
+
|
| 80 |
+
def ensure_authenticated(self) -> bool:
|
| 81 |
+
"""
|
| 82 |
+
Ensure we have a valid token.
|
| 83 |
+
If not, start Device Code flow.
|
| 84 |
+
"""
|
| 85 |
+
token = self.get_valid_token()
|
| 86 |
+
if token:
|
| 87 |
+
return True
|
| 88 |
+
|
| 89 |
+
print("[TokenManager] No valid token found. Starting OAuth flow...")
|
| 90 |
+
new_token = self._client.poll_for_token(
|
| 91 |
+
# This assumes the device flow was already started elsewhere
|
| 92 |
+
# In real usage we would combine with hf_oauth.start_device_flow()
|
| 93 |
+
device_code="" # Placeholder - real implementation would handle full flow
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
if new_token:
|
| 97 |
+
self.save_token(new_token)
|
| 98 |
+
return True
|
| 99 |
+
return False
|
| 100 |
+
|
| 101 |
+
def clear_token(self):
|
| 102 |
+
"""Remove stored token."""
|
| 103 |
+
self._token = None
|
| 104 |
+
if os.path.exists(self.token_file):
|
| 105 |
+
try:
|
| 106 |
+
os.remove(self.token_file)
|
| 107 |
+
except:
|
| 108 |
+
pass
|