Skip to content

Commit bb3dc17

Browse files
tbitcsoz-agent
andcommitted
feat: Phase A — AI Skills Builder + MCP Server Generator
A1: skills_builder.py — build agent skills from natural-language descriptions. Generates SKILL.md + skill.json following SkillNet ontology (name, purpose, activation rules, I/O schema, epistemic contract, tools, tests, stop conditions). CLI: skills build/list/ test/activate. A2: mcp_generator.py — auto-scaffold MCP servers from tool descriptions using FastMCP pattern. Generates server.py, tool_schema.json, README.md. Supports stdio + HTTP transports. A3: 12 tests (6 skills builder, 6 MCP generator) — all passing. REST endpoints: GET /api/esdb/status, GET /api/esdb/counts wired into GovernanceHTTPServer for Kairos ESDB dashboard. Total: 582 Python tests pass, ruff clean. Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent b63035b commit bb3dc17

3 files changed

Lines changed: 512 additions & 0 deletions

File tree

src/specsmith/mcp_generator.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright (c) 2026 Layer1Labs / BitConcepts, LLC.
3+
"""MCP Server Generator — auto-scaffold Model Context Protocol servers.
4+
5+
Generates Python MCP servers from natural-language tool descriptions using
6+
the FastMCP pattern. Each generated server includes a `server.py`,
7+
`tool_schema.json`, and `README.md`.
8+
9+
Usage:
10+
from specsmith.mcp_generator import generate_mcp_server, list_mcp_servers
11+
server = generate_mcp_server("Search USPTO patents by keyword")
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import json
17+
import re
18+
import uuid
19+
from dataclasses import dataclass, field
20+
from pathlib import Path
21+
from typing import Any
22+
23+
24+
@dataclass
25+
class MCPServerSpec:
26+
"""Specification for a generated MCP server."""
27+
28+
id: str
29+
name: str
30+
description: str
31+
tools: list[MCPToolSpec] = field(default_factory=list)
32+
transport: str = "stdio" # "stdio" or "http"
33+
port: int = 8100
34+
35+
def to_dict(self) -> dict[str, Any]:
36+
return {
37+
"id": self.id,
38+
"name": self.name,
39+
"description": self.description,
40+
"tools": [t.to_dict() for t in self.tools],
41+
"transport": self.transport,
42+
"port": self.port,
43+
}
44+
45+
46+
@dataclass
47+
class MCPToolSpec:
48+
"""Specification for a single MCP tool."""
49+
50+
name: str
51+
description: str
52+
input_params: dict[str, str] = field(default_factory=dict)
53+
output_type: str = "text"
54+
55+
def to_dict(self) -> dict[str, Any]:
56+
return {
57+
"name": self.name,
58+
"description": self.description,
59+
"input_params": self.input_params,
60+
"output_type": self.output_type,
61+
}
62+
63+
64+
def _generate_server_id(name: str) -> str:
65+
slug = re.sub(r"[^a-z0-9]+", "-", name.lower().strip())[:30]
66+
return f"mcp-{slug}-{uuid.uuid4().hex[:6]}"
67+
68+
69+
def _generate_server_py(spec: MCPServerSpec) -> str:
70+
"""Generate a Python MCP server file."""
71+
tool_funcs = []
72+
for tool in spec.tools:
73+
params = ", ".join(f"{k}: str" for k in tool.input_params)
74+
func = f'''
75+
@mcp.tool()
76+
def {tool.name}({params}) -> dict:
77+
"""{tool.description}"""
78+
# TODO: implement actual logic
79+
return {{"result": "stub response", "tool": "{tool.name}"}}
80+
'''
81+
tool_funcs.append(func)
82+
83+
transport_line = (
84+
f' mcp.run(transport="streamable-http", host="127.0.0.1", port={spec.port})'
85+
if spec.transport == "http"
86+
else ' mcp.run(transport="stdio")'
87+
)
88+
89+
return f'''#!/usr/bin/env python3
90+
# Auto-generated MCP server: {spec.name}
91+
# Generated by specsmith mcp generate
92+
93+
from mcp.server.fastmcp import FastMCP
94+
95+
mcp = FastMCP("{spec.name}")
96+
{"".join(tool_funcs)}
97+
98+
if __name__ == "__main__":
99+
{transport_line}
100+
'''
101+
102+
103+
def _generate_readme(spec: MCPServerSpec) -> str:
104+
"""Generate README.md for the MCP server."""
105+
tool_list = "\n".join(f"- **{t.name}**: {t.description}" for t in spec.tools)
106+
return f"""# {spec.name}
107+
108+
{spec.description}
109+
110+
## Tools
111+
112+
{tool_list}
113+
114+
## Usage
115+
116+
```bash
117+
# stdio mode
118+
python server.py
119+
120+
# HTTP mode (if configured)
121+
python server.py
122+
```
123+
124+
## Configuration
125+
126+
Add to your MCP client config:
127+
128+
```json
129+
{{
130+
"mcpServers": {{
131+
"{spec.id}": {{
132+
"command": "python",
133+
"args": ["server.py"]
134+
}}
135+
}}
136+
}}
137+
```
138+
"""
139+
140+
141+
def generate_mcp_server(
142+
tool_description: str,
143+
project_dir: str = ".",
144+
transport: str = "stdio",
145+
port: int = 8100,
146+
) -> MCPServerSpec:
147+
"""Generate an MCP server from a natural-language tool description.
148+
149+
Creates a server directory with `server.py`, `tool_schema.json`, and `README.md`.
150+
"""
151+
# Parse description into server + tool spec
152+
name = tool_description.strip()[:50]
153+
server_id = _generate_server_id(name)
154+
tool_name = re.sub(r"[^a-z0-9_]+", "_", name.lower().strip())[:40]
155+
156+
tool = MCPToolSpec(
157+
name=tool_name,
158+
description=tool_description.strip(),
159+
input_params={"query": "Search query or input"},
160+
)
161+
162+
spec = MCPServerSpec(
163+
id=server_id,
164+
name=name,
165+
description=tool_description.strip(),
166+
tools=[tool],
167+
transport=transport,
168+
port=port,
169+
)
170+
171+
# Create server directory
172+
server_dir = Path(project_dir).resolve() / ".specsmith" / "mcp-servers" / server_id
173+
server_dir.mkdir(parents=True, exist_ok=True)
174+
175+
# Write files
176+
(server_dir / "server.py").write_text(_generate_server_py(spec), encoding="utf-8")
177+
(server_dir / "tool_schema.json").write_text(
178+
json.dumps(spec.to_dict(), indent=2, ensure_ascii=False), encoding="utf-8"
179+
)
180+
(server_dir / "README.md").write_text(_generate_readme(spec), encoding="utf-8")
181+
182+
return spec
183+
184+
185+
def list_mcp_servers(project_dir: str = ".") -> list[MCPServerSpec]:
186+
"""List all generated MCP servers."""
187+
servers_dir = Path(project_dir).resolve() / ".specsmith" / "mcp-servers"
188+
if not servers_dir.is_dir():
189+
return []
190+
servers: list[MCPServerSpec] = []
191+
for server_dir in sorted(servers_dir.iterdir()):
192+
schema_path = server_dir / "tool_schema.json"
193+
if schema_path.is_file():
194+
try:
195+
raw = json.loads(schema_path.read_text(encoding="utf-8"))
196+
servers.append(
197+
MCPServerSpec(
198+
id=raw.get("id", ""),
199+
name=raw.get("name", ""),
200+
description=raw.get("description", ""),
201+
transport=raw.get("transport", "stdio"),
202+
port=raw.get("port", 8100),
203+
)
204+
)
205+
except (OSError, ValueError):
206+
continue
207+
return servers
208+
209+
210+
__all__ = ["MCPServerSpec", "MCPToolSpec", "generate_mcp_server", "list_mcp_servers"]

0 commit comments

Comments
 (0)