Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions docs/mcp-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,23 @@ Validate a structure configuration YAML file.

## Usage

### Starting the MCP Server
### Starting the MCP Server (FastMCP stdio / http / sse)

To start the MCP server for stdio communication:
The MCP server uses FastMCP (v2.0+) and can run over stdio, http, or sse transports.

- stdio (default):
```bash
struct mcp --server
struct mcp --server --transport stdio
```

- HTTP (StreamableHTTP):
```bash
struct mcp --server --transport http --host 127.0.0.1 --port 9000 --path /mcp
```

- SSE:
```bash
struct mcp --server --transport sse --host 0.0.0.0 --port 8080 --path /events
```

### Command Line Integration
Expand Down Expand Up @@ -140,10 +151,10 @@ For Cline (VS Code extension), add to your `.cline_mcp_settings.json`:

### Custom MCP Client Integration

For any MCP-compatible client, use these connection parameters:
For any MCP-compatible client, connect over stdio with your preferred SDK:

```javascript
// Node.js example
// Node.js example (MCP JS SDK)
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';

Expand All @@ -166,7 +177,7 @@ await client.connect(transport);
```

```python
# Python example
# Python example (MCP Python SDK)
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
Expand All @@ -181,12 +192,11 @@ async def main():
async with ClientSession(read, write) as session:
await session.initialize()

# List available tools
tools = await session.list_tools()
print(f"Available tools: {[tool.name for tool in tools.tools]}")
print([t.name for t in tools.tools])

# Call a tool
result = await session.call_tool("list_structures", {})
# FastMCP tools return plain text content
print(result.content[0].text)

if __name__ == "__main__":
Expand Down Expand Up @@ -308,7 +318,8 @@ Then configure your MCP client:

### Step 1: Install struct with MCP support
```bash
pip install struct[mcp] # or pip install struct && pip install mcp
pip install fastmcp>=2.0
# (your MCP client may also require installing the MCP SDK, e.g., `pip install mcp`)
```

### Step 2: Test MCP server
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ google-cloud
google-api-core
cachetools
pydantic-ai
mcp
fastmcp>=2.0
84 changes: 73 additions & 11 deletions struct_module/commands/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,97 @@
from struct_module.mcp_server import StructMCPServer


# MCP command class for starting the MCP server
# MCP command class for starting the MCP server (FastMCP stdio only)
class MCPCommand(Command):
def __init__(self, parser):
super().__init__(parser)
parser.description = "MCP (Model Context Protocol) support for struct tool"
parser.description = "MCP (Model Context Protocol) using FastMCP transports (stdio, http, sse)"
parser.add_argument('--server', action='store_true',
help='Start the MCP server for stdio communication')
help='Start the MCP server')
parser.add_argument('--transport', choices=['stdio', 'http', 'sse'], default='stdio',
help='Transport protocol for the MCP server (default: stdio)')
# HTTP/SSE options
parser.add_argument('--host', type=str, default='127.0.0.1', help='Host to bind for HTTP/SSE transports')
parser.add_argument('--port', type=int, default=8000, help='Port to bind for HTTP/SSE transports')
parser.add_argument('--path', type=str, default='/mcp', help='Endpoint path for HTTP/SSE transports')
parser.add_argument('--uvicorn-log-level', dest='uvicorn_log_level', type=str, default=None,
help='Log level for the HTTP server (e.g., info, warning, error)')
parser.add_argument('--stateless-http', action='store_true', default=None,
help='Use stateless HTTP mode (HTTP transport only)')
parser.add_argument('--no-banner', dest='show_banner', action='store_false', default=True,
help='Disable FastMCP startup banner')
# Debugging options
parser.add_argument('--debug', action='store_true', help='Enable debug mode (sets struct and FastMCP loggers to DEBUG by default)')
parser.add_argument('--fastmcp-log-level', dest='fastmcp_log_level', type=str, default=None,
help='Log level for FastMCP internals (e.g., DEBUG, INFO). Overrides --debug for FastMCP if provided')
parser.set_defaults(func=self.execute)

def execute(self, args):
if args.server:
self.logger.info("Starting MCP server for struct tool")
asyncio.run(self._start_mcp_server())
self.logger.info(
f"Starting FastMCP server for struct tool (transport={args.transport})"
)
asyncio.run(self._start_mcp_server(args))
else:
print("MCP (Model Context Protocol) support for struct tool")
print("MCP (Model Context Protocol) support for struct tool (FastMCP)")
print("\nAvailable options:")
print(" --server Start the MCP server for stdio communication")
print(" --server Start the MCP server")
print(" --transport {stdio|http|sse} Transport protocol (default: stdio)")
print(" --host HOST Host for HTTP/SSE (default: 127.0.0.1)")
print(" --port PORT Port for HTTP/SSE (default: 8000)")
print(" --path /PATH Endpoint path for HTTP/SSE (default: /mcp)")
print(" --stateless-http Enable stateless HTTP mode (HTTP only)")
print(" --no-banner Disable FastMCP banner")
print(" --debug Enable debug mode (struct + FastMCP DEBUG; uvicorn=debug)")
print(" --fastmcp-log-level LVL Set FastMCP logger level (overrides --debug for FastMCP)")
print("\nMCP tools available:")
print(" - list_structures: List all available structure definitions")
print(" - get_structure_info: Get detailed information about a structure")
print(" - generate_structure: Generate structures with various options")
print(" - validate_structure: Validate structure configuration files")
print("\nTo integrate with MCP clients, use: struct mcp --server")
print("\nExamples:")
print(" struct mcp --server --transport stdio --debug")
print(" struct mcp --server --transport http --host 127.0.0.1 --port 9000 --path /mcp --uvicorn-log-level debug")
print(" struct mcp --server --transport sse --host 0.0.0.0 --port 8080 --path /events --fastmcp-log-level DEBUG")

async def _start_mcp_server(self):
"""Start the MCP server."""
async def _start_mcp_server(self, args=None):
"""Start the MCP server using the selected transport."""
try:
server = StructMCPServer()
await server.run()
transport = getattr(args, 'transport', 'stdio') if args else 'stdio'
# Map CLI args to server.run kwargs
run_kwargs = {
"transport": transport,
"show_banner": getattr(args, 'show_banner', True) if args else True,
}
# Determine FastMCP logger level
fastmcp_log_level = None
if args:
fastmcp_log_level = getattr(args, 'fastmcp_log_level', None)
if not fastmcp_log_level and getattr(args, 'debug', False):
fastmcp_log_level = 'DEBUG'
if fastmcp_log_level:
run_kwargs["fastmcp_log_level"] = fastmcp_log_level

if transport in {"http", "sse"}:
# uvicorn expects lowercase levels like "info"/"debug"
uvicorn_level = None
if args:
uvicorn_level = getattr(args, 'uvicorn_log_level', None)
if not uvicorn_level and getattr(args, 'debug', False):
uvicorn_level = 'debug'
if not uvicorn_level:
# Default to args.log if provided, else None
uvicorn_level = getattr(args, 'log', None)
run_kwargs.update({
"host": getattr(args, 'host', None),
"port": getattr(args, 'port', None),
"path": getattr(args, 'path', None),
"log_level": (uvicorn_level.lower() if isinstance(uvicorn_level, str) else uvicorn_level),
})
if transport == "http":
run_kwargs["stateless_http"] = getattr(args, 'stateless_http', None)
await server.run(**run_kwargs)
except Exception as e:
self.logger.error(f"Error starting MCP server: {e}")
raise
12 changes: 11 additions & 1 deletion struct_module/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import argparse
import logging
import os
from dotenv import load_dotenv
from struct_module.utils import read_config_file, merge_configs
from struct_module.commands.generate import GenerateCommand
Expand Down Expand Up @@ -65,7 +66,16 @@ def main():
file_config = read_config_file(args.config_file)
args = argparse.Namespace(**merge_configs(file_config, args))

logging_level = getattr(logging, getattr(args, 'log', 'INFO').upper(), logging.INFO)
# Resolve logging level precedence: STRUCT_LOG_LEVEL env > --debug (if present) > --log
env_level = os.getenv('STRUCT_LOG_LEVEL')
if env_level:
logging_level = getattr(logging, env_level.upper(), logging.INFO)
else:
# Some commands (like mcp) may add a --debug flag; respect it
if getattr(args, 'debug', False):
logging_level = logging.DEBUG
else:
logging_level = getattr(logging, getattr(args, 'log', 'INFO').upper(), logging.INFO)

configure_logging(level=logging_level, log_file=getattr(args, 'log_file', None))

Expand Down
Loading
Loading