"""Interactive Gradio UI for Roslyn-Stone MCP Server Provides dynamic testing interface for MCP tools, resources, and prompts. """ from __future__ import annotations import json import os from typing import Any import gradio as gr import httpx from pygments import highlight from pygments.formatters import HtmlFormatter from pygments.lexers import CSharpLexer, JsonLexer # Maximum iterations for tool calls to prevent infinite loops MAX_TOOL_ITERATIONS = 10 # MCP Client for HTTP transport class McpHttpClient: """Simple MCP HTTP client for interacting with the server.""" def __init__(self, base_url: str) -> None: """Initialize the MCP HTTP client. Args: base_url: Base URL of the MCP server. """ self.base_url = base_url.rstrip("/") self.mcp_url = f"{self.base_url}/mcp" self.client = httpx.Client(timeout=30.0) def close(self) -> None: """Close the HTTP client and release resources.""" self.client.close() def __enter__(self) -> McpHttpClient: """Enter context manager.""" return self def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: """Exit context manager.""" self.close() def _send_request(self, method: str, params: dict | None = None) -> dict[str, Any]: """Send a JSON-RPC request to the MCP server.""" request_data = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}} try: response = self.client.post(self.mcp_url, json=request_data) response.raise_for_status() # MCP HTTP transport uses Server-Sent Events (SSE) format # Response format: "event: message\ndata: {json}\n\n" response_text = response.text # Parse SSE format if response_text.startswith("event:"): lines = response_text.strip().split("\n") for line in lines: if line.startswith("data: "): json_data = line[6:] # Remove "data: " prefix result = json.loads(json_data) if "error" in result: return {"error": result["error"]} return result.get("result", {}) # type: ignore[no-any-return] else: # Fallback to regular JSON result = response.json() if "error" in result: return {"error": result["error"]} return result.get("result", {}) # type: ignore[no-any-return] except (httpx.HTTPError, json.JSONDecodeError) as e: return {"error": str(e)} except Exception as e: # Re-raise system-exiting exceptions if isinstance(e, (KeyboardInterrupt, SystemExit)): raise return {"error": str(e)} return {"error": "Unknown error"} def list_tools(self) -> list[dict[str, Any]]: """List all available MCP tools.""" result = self._send_request("tools/list") if "error" in result: return [] tools: list[dict[str, Any]] = result.get("tools", []) return tools def list_resources(self) -> list[dict[str, Any]]: """List all available MCP resource templates.""" result = self._send_request("resources/templates/list") if "error" in result: return [] templates: list[dict[str, Any]] = result.get("resourceTemplates", []) return templates def list_prompts(self) -> list[dict[str, Any]]: """List all available MCP prompts.""" result = self._send_request("prompts/list") if "error" in result: return [] prompts: list[dict[str, Any]] = result.get("prompts", []) return prompts def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]: """Call an MCP tool with given arguments.""" return self._send_request("tools/call", {"name": name, "arguments": arguments}) def read_resource(self, uri: str) -> dict[str, Any]: """Read an MCP resource.""" return self._send_request("resources/read", {"uri": uri}) def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]: """Get an MCP prompt.""" return self._send_request("prompts/get", {"name": name, "arguments": arguments or {}}) def format_csharp_code(code: str) -> str: """Format C# code with syntax highlighting.""" try: formatter = HtmlFormatter(style="monokai", noclasses=True, cssclass="highlight") highlighted = highlight(code, CSharpLexer(), formatter) return f'
{code}'
def format_json_output(data: object) -> str:
"""Format JSON output with syntax highlighting."""
try:
json_str = json.dumps(data, indent=2)
formatter = HtmlFormatter(style="monokai", noclasses=True, cssclass="highlight")
highlighted = highlight(json_str, JsonLexer(), formatter)
return f'{json.dumps(data, indent=2)}'
def call_openai_chat(
messages: list[dict], api_key: str, model: str, tools: list[dict], mcp_client: McpHttpClient
) -> str:
"""Call OpenAI API with MCP tools."""
try:
import openai
client = openai.OpenAI(api_key=api_key)
for _ in range(MAX_TOOL_ITERATIONS):
response = client.chat.completions.create(
model=model,
messages=messages, # type: ignore[arg-type]
tools=tools if tools else None, # type: ignore[arg-type]
)
message = response.choices[0].message
# Handle tool calls
if message.tool_calls:
for tool_call in message.tool_calls:
tool_name = tool_call.function.name # type: ignore[union-attr]
tool_args = json.loads(tool_call.function.arguments) # type: ignore[union-attr]
# Call MCP tool
result = mcp_client.call_tool(tool_name, tool_args)
# Add tool result to messages
messages.append(
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": tool_call.id,
"function": {
"name": tool_name,
"arguments": tool_call.function.arguments, # type: ignore[union-attr]
},
"type": "function",
}
],
}
)
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result),
}
)
# Continue loop to make another call with tool results
continue
return message.content or "No response"
return "Error: Maximum tool call iterations exceeded"
except Exception as e:
return f"Error: {e!s}"
def call_anthropic_chat(
messages: list[dict], api_key: str, model: str, tools: list[dict], mcp_client: McpHttpClient
) -> str:
"""Call Anthropic API with MCP tools."""
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
# Convert messages format
anthropic_messages = []
for msg in messages:
if msg["role"] == "system":
continue
anthropic_messages.append({"role": msg["role"], "content": msg["content"]})
for _ in range(MAX_TOOL_ITERATIONS):
response = client.messages.create(
model=model,
max_tokens=4096,
messages=anthropic_messages, # type: ignore[arg-type]
tools=tools if tools else None, # type: ignore[arg-type]
)
# Handle tool calls
if response.stop_reason == "tool_use":
for content in response.content:
if content.type == "tool_use":
tool_name = content.name
tool_args = content.input
# Call MCP tool
result = mcp_client.call_tool(tool_name, tool_args)
# Add tool result and continue
anthropic_messages.append(
{"role": "assistant", "content": response.content}
)
anthropic_messages.append(
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": content.id,
"content": json.dumps(result),
}
],
}
)
# Continue loop to make another call with tool results
continue
return response.content[0].text if response.content else "No response" # type: ignore[union-attr]
return "Error: Maximum tool call iterations exceeded"
except Exception as e:
return f"Error: {e!s}"
def call_gemini_chat(
messages: list[dict], api_key: str, model: str, tools: list[dict], mcp_client: McpHttpClient
) -> str:
"""Call Google Gemini API with MCP tools."""
try:
import google.generativeai as genai
genai.configure(api_key=api_key)
model_instance = genai.GenerativeModel(model)
# Convert messages to Gemini format
prompt = "\n\n".join(
[f"{msg['role']}: {msg['content']}" for msg in messages if msg.get("content")]
)
response = model_instance.generate_content(prompt)
return response.text # type: ignore[no-any-return]
except Exception as e:
return f"Error: {e!s}"
def call_huggingface_chat(
messages: list[dict], api_key: str, model: str, mcp_client: McpHttpClient
) -> str:
"""Call HuggingFace Inference API (supports serverless)."""
try:
from huggingface_hub import InferenceClient
# Use provided key, or fall back to HF_API_KEY/HF_TOKEN from environment (HF Spaces secrets)
token: str | None = api_key
if not token:
token = os.environ.get("HF_API_KEY") or os.environ.get("HF_TOKEN")
# Create client (serverless works without token, but rate limited)
client = InferenceClient(token=token) if token else InferenceClient()
# Convert messages to chat format
chat_messages = []
for msg in messages:
if msg.get("content"):
chat_messages.append({"role": msg["role"], "content": msg["content"]})
# Call chat completion
response: str = ""
for message in client.chat_completion(
messages=chat_messages,
model=model if model else "meta-llama/Llama-3.2-3B-Instruct",
max_tokens=2048,
stream=True,
):
if message.choices and message.choices[0].delta.content:
response += message.choices[0].delta.content
return response if response else "No response"
except Exception as e:
return f"Error: {e!s}"
def get_mcp_endpoint_url() -> str:
"""Get the public MCP endpoint URL for clients to connect to.
On HuggingFace Spaces, this returns the embed URL.
Otherwise, returns the configured base URL.
"""
# Check for HuggingFace Space
space_id = os.environ.get("SPACE_ID")
if space_id:
# Format: username/repo -> username-repo.hf.space
space_subdomain = space_id.replace("/", "-").lower()
return f"https://{space_subdomain}.hf.space/mcp"
# Check for custom BASE_URL
base_url = os.environ.get("BASE_URL")
if base_url:
return f"{base_url.rstrip('/')}/mcp"
# Check for ASPNETCORE_URLS
aspnetcore_urls = os.environ.get("ASPNETCORE_URLS")
if aspnetcore_urls:
first_url = aspnetcore_urls.split(";")[0]
# Replace wildcards with localhost for display
first_url = first_url.replace("http://+:", "http://localhost:")
first_url = first_url.replace("http://*:", "http://localhost:")
first_url = first_url.replace("http://0.0.0.0:", "http://localhost:")
return f"{first_url.rstrip('/')}/mcp"
return "http://localhost:7071/mcp"
def create_landing_page(base_url: str | None = None) -> gr.Blocks:
"""Create the interactive Gradio UI for MCP server testing.
Args:
base_url: The base URL of the MCP server (e.g., http://localhost:7071)
Returns:
A Gradio Blocks interface
"""
import atexit
if base_url is None:
base_url = "http://localhost:7071"
# Initialize MCP client
mcp_client = McpHttpClient(base_url)
# Get the public MCP endpoint URL for display
mcp_endpoint = get_mcp_endpoint_url()
# Register cleanup to close the HTTP client on exit
def cleanup() -> None:
mcp_client.close()
atexit.register(cleanup)
# CSS for better styling with syntax highlighting support
pygments_css = HtmlFormatter(style="monokai").get_style_defs(".highlight")
custom_css = f"""
/* Pygments syntax highlighting */
{pygments_css}
/* Main container styling */
.gradio-container {{
max-width: 1400px !important;
margin: auto;
}}
/* Header styling */
h1 {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 800 !important;
font-size: 2.5rem !important;
}}
/* Tab styling */
.tab-nav button {{
font-size: 16px !important;
padding: 12px 24px !important;
border-radius: 8px 8px 0 0 !important;
transition: all 0.3s ease;
}}
.tab-nav button[aria-selected="true"] {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
font-weight: 600 !important;
box-shadow: 0 4px 6px rgba(102, 126, 234, 0.3);
}}
/* Card styling */
.tool-card, .resource-card, .prompt-card {{
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
margin: 15px 0;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}}
.tool-card:hover, .resource-card:hover, .prompt-card:hover {{
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}}
/* Button styling */
button {{
transition: all 0.3s ease !important;
}}
button:hover {{
transform: translateY(-1px) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important;
}}
/* Input styling */
.param-input {{
margin: 8px 0;
border-radius: 6px;
}}
/* Code editor styling */
.code-editor {{
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace !important;
font-size: 14px !important;
line-height: 1.5 !important;
}}
/* Result box styling */
.result-box {{
background-color: #1e1e1e;
border-radius: 8px;
padding: 10px;
font-family: monospace;
white-space: pre-wrap;
max-height: 600px;
overflow-y: auto;
}}
/* Status indicator */
.status-indicator {{
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #00ff00;
box-shadow: 0 0 10px #00ff00;
animation: pulse 2s infinite;
}}
@keyframes pulse {{
0%, 100% {{
opacity: 1;
}}
50% {{
opacity: 0.5;
}}
}}
/* Highlight important text */
.highlight-text {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 600;
}}
"""
# In Gradio 6.0+, Blocks() constructor simplified - theme and css moved to launch()
with gr.Blocks(title="Roslyn-Stone MCP Testing UI") as demo:
# Inject custom CSS via Markdown/HTML component instead
gr.HTML(f"")
# State management for storing tools, resources, and prompts data
tools_state = gr.State({})
resources_state = gr.State({})
prompts_state = gr.State({})
gr.Markdown(
"""
# đǍ Roslyn-Stone MCP Server - Interactive Testing UI
Welcome to the **interactive testing interface** for Roslyn-Stone MCP Server.
This UI dynamically loads all available tools, resources, and prompts from the MCP server.
"""
)
with gr.Tabs():
# Setup Tab (Welcome page with connection instructions)
with gr.Tab("đ Setup"):
gr.Markdown(
f"""
## Welcome to Roslyn-Stone MCP Server!
This is an interactive C# sandbox that provides AI tools through the Model Context Protocol (MCP).
### đ MCP Server Endpoint
Connect your MCP client to this server using the following endpoint:
```
{mcp_endpoint}
```
---
### đ Quick Setup Instructions
#### Claude Desktop
Add to your `claude_desktop_config.json`:
```json
{{
"mcpServers": {{
"roslyn-stone": {{
"command": "npx",
"args": [
"mcp-remote",
"{mcp_endpoint}"
]
}}
}}
}}
```
#### VS Code with Copilot
Add to your VS Code `settings.json`:
```json
{{
"github.copilot.chat.mcpServers": {{
"roslyn-stone": {{
"type": "http",
"url": "{mcp_endpoint}"
}}
}}
}}
```
#### Using mcp-remote (Any MCP Client)
If your client doesn't support HTTP transport directly, use `mcp-remote` as a bridge:
```bash
npx mcp-remote {mcp_endpoint}
```
---
### đ ī¸ Available Capabilities
| Category | Description |
|----------|-------------|
| **đ§ Tools** | Execute C# code, validate syntax, search NuGet packages, load assemblies |
| **đ Resources** | Access .NET documentation, NuGet package info, REPL state |
| **đŦ Prompts** | Get guidance and best practices for C# development |
| **đ¤ Chat** | Interactive chat with AI using MCP tools (try the Chat tab!) |
---
### đ Security Note
â ī¸ This server can execute arbitrary C# code. When self-hosting:
- Run in isolated containers or sandboxes
- Implement authentication and rate limiting
- Restrict network access as needed
- Monitor resource usage
---
**Explore the tabs above to test tools, browse resources, view prompts, or chat with AI!**
"""
)
# Tools Tab
with gr.Tab("đ§ Tools"):
gr.Markdown("### Execute MCP Tools")
gr.Markdown(
"Tools perform operations like executing C# code, loading NuGet packages, etc."
)
refresh_tools_btn = gr.Button("đ Refresh Tools", size="sm")
tools_status = gr.Markdown("Click 'Refresh Tools' to load available tools...")
with gr.Row():
with gr.Column(scale=1):
tool_dropdown = gr.Dropdown(
label="Select Tool", choices=[], interactive=True
)
tool_description = gr.Textbox(
label="Tool Description",
lines=5,
interactive=False,
max_lines=10,
)
tool_params_json = gr.Code(
label="Tool Parameters (JSON)", language="json", value="{}", lines=10
)
execute_tool_btn = gr.Button("âļī¸ Execute Tool", variant="primary", size="lg")
with gr.Column(scale=1):
tool_result = gr.Code(
label="Tool Result (JSON)", language="json", lines=20, interactive=False
)
# Tool examples with better formatting
with gr.Accordion("đ Example Tool Calls", open=True):
gr.Markdown("### C# Code Execution Examples")
gr.HTML("""
{
"code": "var x = 10; x * 2",
"createContext": false
}
{
"code": "var x = 10; x * 2"
}
{
"query": "json",
"skip": 0,
"take": 10
}
doc://System.String - String class documentation
doc://System.Linq.Enumerable - LINQ methods
doc://System.Collections.Generic.List`1 - List<T> docs
nuget://search?q=json - Search JSON packages
nuget://search?q=http&take=5 - Search HTTP packages (5 results)
nuget://packages/Newtonsoft.Json/versions - All versions
nuget://packages/Newtonsoft.Json/readme - Package README
repl://state - Current REPL environment info
repl://info - REPL capabilities