Skip to content

Commit 3464fbf

Browse files
committed
Merge branch 'main' of github.com:httpdss/struct
2 parents d034aea + 8499e00 commit 3464fbf

7 files changed

Lines changed: 453 additions & 588 deletions

File tree

docs/mcp-integration.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,23 @@ Validate a structure configuration YAML file.
8181

8282
## Usage
8383

84-
### Starting the MCP Server
84+
### Starting the MCP Server (FastMCP stdio / http / sse)
8585

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

88+
- stdio (default):
8889
```bash
89-
struct mcp --server
90+
struct mcp --server --transport stdio
91+
```
92+
93+
- HTTP (StreamableHTTP):
94+
```bash
95+
struct mcp --server --transport http --host 127.0.0.1 --port 9000 --path /mcp
96+
```
97+
98+
- SSE:
99+
```bash
100+
struct mcp --server --transport sse --host 0.0.0.0 --port 8080 --path /events
90101
```
91102

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

141152
### Custom MCP Client Integration
142153

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

145156
```javascript
146-
// Node.js example
157+
// Node.js example (MCP JS SDK)
147158
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
148159
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
149160

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

168179
```python
169-
# Python example
180+
# Python example (MCP Python SDK)
170181
import asyncio
171182
from mcp import ClientSession, StdioServerParameters
172183
from mcp.client.stdio import stdio_client
@@ -181,12 +192,11 @@ async def main():
181192
async with ClientSession(read, write) as session:
182193
await session.initialize()
183194

184-
# List available tools
185195
tools = await session.list_tools()
186-
print(f"Available tools: {[tool.name for tool in tools.tools]}")
196+
print([t.name for t in tools.tools])
187197

188-
# Call a tool
189198
result = await session.call_tool("list_structures", {})
199+
# FastMCP tools return plain text content
190200
print(result.content[0].text)
191201

192202
if __name__ == "__main__":
@@ -308,7 +318,8 @@ Then configure your MCP client:
308318

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

314325
### Step 2: Test MCP server

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ google-cloud
1111
google-api-core
1212
cachetools
1313
pydantic-ai
14-
mcp
14+
fastmcp>=2.0

struct_module/commands/mcp.py

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,97 @@
44
from struct_module.mcp_server import StructMCPServer
55

66

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

1632
def execute(self, args):
1733
if args.server:
18-
self.logger.info("Starting MCP server for struct tool")
19-
asyncio.run(self._start_mcp_server())
34+
self.logger.info(
35+
f"Starting FastMCP server for struct tool (transport={args.transport})"
36+
)
37+
asyncio.run(self._start_mcp_server(args))
2038
else:
21-
print("MCP (Model Context Protocol) support for struct tool")
39+
print("MCP (Model Context Protocol) support for struct tool (FastMCP)")
2240
print("\nAvailable options:")
23-
print(" --server Start the MCP server for stdio communication")
41+
print(" --server Start the MCP server")
42+
print(" --transport {stdio|http|sse} Transport protocol (default: stdio)")
43+
print(" --host HOST Host for HTTP/SSE (default: 127.0.0.1)")
44+
print(" --port PORT Port for HTTP/SSE (default: 8000)")
45+
print(" --path /PATH Endpoint path for HTTP/SSE (default: /mcp)")
46+
print(" --stateless-http Enable stateless HTTP mode (HTTP only)")
47+
print(" --no-banner Disable FastMCP banner")
48+
print(" --debug Enable debug mode (struct + FastMCP DEBUG; uvicorn=debug)")
49+
print(" --fastmcp-log-level LVL Set FastMCP logger level (overrides --debug for FastMCP)")
2450
print("\nMCP tools available:")
2551
print(" - list_structures: List all available structure definitions")
2652
print(" - get_structure_info: Get detailed information about a structure")
2753
print(" - generate_structure: Generate structures with various options")
2854
print(" - validate_structure: Validate structure configuration files")
29-
print("\nTo integrate with MCP clients, use: struct mcp --server")
55+
print("\nExamples:")
56+
print(" struct mcp --server --transport stdio --debug")
57+
print(" struct mcp --server --transport http --host 127.0.0.1 --port 9000 --path /mcp --uvicorn-log-level debug")
58+
print(" struct mcp --server --transport sse --host 0.0.0.0 --port 8080 --path /events --fastmcp-log-level DEBUG")
3059

31-
async def _start_mcp_server(self):
32-
"""Start the MCP server."""
60+
async def _start_mcp_server(self, args=None):
61+
"""Start the MCP server using the selected transport."""
3362
try:
3463
server = StructMCPServer()
35-
await server.run()
64+
transport = getattr(args, 'transport', 'stdio') if args else 'stdio'
65+
# Map CLI args to server.run kwargs
66+
run_kwargs = {
67+
"transport": transport,
68+
"show_banner": getattr(args, 'show_banner', True) if args else True,
69+
}
70+
# Determine FastMCP logger level
71+
fastmcp_log_level = None
72+
if args:
73+
fastmcp_log_level = getattr(args, 'fastmcp_log_level', None)
74+
if not fastmcp_log_level and getattr(args, 'debug', False):
75+
fastmcp_log_level = 'DEBUG'
76+
if fastmcp_log_level:
77+
run_kwargs["fastmcp_log_level"] = fastmcp_log_level
78+
79+
if transport in {"http", "sse"}:
80+
# uvicorn expects lowercase levels like "info"/"debug"
81+
uvicorn_level = None
82+
if args:
83+
uvicorn_level = getattr(args, 'uvicorn_log_level', None)
84+
if not uvicorn_level and getattr(args, 'debug', False):
85+
uvicorn_level = 'debug'
86+
if not uvicorn_level:
87+
# Default to args.log if provided, else None
88+
uvicorn_level = getattr(args, 'log', None)
89+
run_kwargs.update({
90+
"host": getattr(args, 'host', None),
91+
"port": getattr(args, 'port', None),
92+
"path": getattr(args, 'path', None),
93+
"log_level": (uvicorn_level.lower() if isinstance(uvicorn_level, str) else uvicorn_level),
94+
})
95+
if transport == "http":
96+
run_kwargs["stateless_http"] = getattr(args, 'stateless_http', None)
97+
await server.run(**run_kwargs)
3698
except Exception as e:
3799
self.logger.error(f"Error starting MCP server: {e}")
38100
raise

struct_module/main.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import argparse
22
import logging
3+
import os
34
from dotenv import load_dotenv
45
from struct_module.utils import read_config_file, merge_configs
56
from struct_module.commands.generate import GenerateCommand
@@ -65,7 +66,16 @@ def main():
6566
file_config = read_config_file(args.config_file)
6667
args = argparse.Namespace(**merge_configs(file_config, args))
6768

68-
logging_level = getattr(logging, getattr(args, 'log', 'INFO').upper(), logging.INFO)
69+
# Resolve logging level precedence: STRUCT_LOG_LEVEL env > --debug (if present) > --log
70+
env_level = os.getenv('STRUCT_LOG_LEVEL')
71+
if env_level:
72+
logging_level = getattr(logging, env_level.upper(), logging.INFO)
73+
else:
74+
# Some commands (like mcp) may add a --debug flag; respect it
75+
if getattr(args, 'debug', False):
76+
logging_level = logging.DEBUG
77+
else:
78+
logging_level = getattr(logging, getattr(args, 'log', 'INFO').upper(), logging.INFO)
6979

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

0 commit comments

Comments
 (0)