Skip to content

Commit 32edeb6

Browse files
feat: MCP server for AI agent integration (v1.4.0)
- New MCP server with 5 tools: convert, diff, check, formats, detect_format - New CLI subcommand: schemaforge mcp (stdio mode / --sse for HTTP) - Tools accessible to Claude Desktop, Cursor, and other MCP clients - Optional dependency: pip install schemaforge[mcp] - Added MCP documentation section to README - 14 new tests for MCP server, 233/233 tests passing - Bumped version to 1.4.0
1 parent 0dd1552 commit 32edeb6

5 files changed

Lines changed: 417 additions & 2 deletions

File tree

README.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![Python](https://img.shields.io/pypi/pyversions/schemaforge)](https://pypi.org/project/schemaforge/)
77
[![License](https://img.shields.io/pypi/l/schemaforge)](https://github.com/Coding-Dev-Tools/schemaforge/blob/main/LICENSE)
88
[![CI](https://github.com/Coding-Dev-Tools/schemaforge/actions/workflows/test.yml/badge.svg)](https://github.com/Coding-Dev-Tools/schemaforge/actions/workflows/test.yml)
9-
[![Tests](https://img.shields.io/badge/tests-205%20passing-brightgreen)](https://github.com/Coding-Dev-Tools/schemaforge)
9+
[![Tests](https://img.shields.io/badge/tests-233%20passing-brightgreen)](https://github.com/Coding-Dev-Tools/schemaforge)
1010

1111
**Why SchemaForge?** Every major ORM migration is a one-way street. Prisma introspects SQL but can't export back. Drizzle users manually rewrite schemas when switching ORMs. TypeORM developers are locked into decorator syntax. SchemaForge is the first tool to do **bidirectional, lossless conversion** between 9 schema formats — with a shared internal representation that guarantees roundtrip fidelity.
1212

@@ -248,6 +248,57 @@ Each fixture demonstrates the same blog schema so you can compare ORM syntax sid
248248
- **Relation preservation** — indexes, unique constraints maintained across all conversions
249249
- **Custom type handling** — dialect-specific types (JSONB, etc.) pass through via CUSTOM type
250250

251+
## MCP Server
252+
253+
SchemaForge includes an **MCP (Model Context Protocol) server** that exposes all schema operations as tools for AI agents. This allows AI coding assistants like Claude Code, Cursor, and others to convert, diff, and check schemas directly.
254+
255+
```bash
256+
# Install with MCP support
257+
pip install schemaforge[mcp]
258+
259+
# Start the server (stdio mode — default for AI clients)
260+
schemaforge mcp
261+
262+
# Start as SSE HTTP server
263+
schemaforge mcp --sse --port 8000
264+
```
265+
266+
### Available Tools
267+
268+
| Tool | Description |
269+
|------|-------------|
270+
| `convert` | Convert a schema between any two of the 9 formats |
271+
| `diff` | Compare two schemas and show differences |
272+
| `check` | Verify schema consistency across a directory |
273+
| `formats` | List all supported formats with descriptions |
274+
| `detect_format` | Identify format from filename |
275+
276+
### Configuration for AI Clients
277+
278+
**Claude Desktop** (`claude_desktop_config.json`):
279+
```json
280+
{
281+
"mcpServers": {
282+
"schemaforge": {
283+
"command": "schemaforge",
284+
"args": ["mcp"]
285+
}
286+
}
287+
}
288+
```
289+
290+
**Cursor**: Add to `.cursor/mcp.json`:
291+
```json
292+
{
293+
"mcpServers": {
294+
"schemaforge": {
295+
"command": "schemaforge",
296+
"args": ["mcp"]
297+
}
298+
}
299+
}
300+
```
301+
251302
## Roadmap
252303

253304
| Version | Features |
@@ -265,6 +316,7 @@ Each fixture demonstrates the same blog schema so you can compare ORM syntax sid
265316
| v1.1.0 | Custom type mapping configuration (YAML/JSON overrides) |
266317
| v1.2.0 | JSON Schema support (8th format) |
267318
| **v1.3.0** | **GraphQL SDL support (9th format)** |
319+
| v1.4.0 | Schema consistency check, CI/CD workflow, MCP server |
268320

269321
### Planned
270322

@@ -297,6 +349,7 @@ SchemaForge is one of eight tools in the Revenue Holdings suite. One license cov
297349
| Alembic migration generation | — | ✓ | ✓ | ✓ | ✓ |
298350
| JSON Schema import/export | — | ✓ | ✓ | ✓ | ✓ |
299351
| GraphQL SDL import/export | — | ✓ | ✓ | ✓ | ✓ |
352+
| MCP server (AI agent tools) | ✓ | ✓ | ✓ | ✓ | ✓ |
300353
| Custom type mappings | — | ✓ | ✓ | ✓ | ✓ |
301354
| Batch directory conversion | — | ✓ | ✓ | ✓ | ✓ |
302355
| Team shared type mappings | — | — | — | ✓ | ✓ |

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "schemaforge"
7-
version = "1.3.0"
7+
version = "1.4.0"
88
description = "Bidirectional ORM schema converter — convert between SQL DDL, Prisma, Drizzle, TypeORM, Django, SQLAlchemy, Alembic, JSON Schema, and GraphQL SDL with zero-loss roundtripping"
99
readme = "README.md"
1010
requires-python = ">=3.10"
@@ -24,6 +24,7 @@ dev = [
2424
"pytest>=7.0",
2525
"pytest-cov>=4.0",
2626
]
27+
mcp = ["mcp>=1.0"]
2728

2829
[project.scripts]
2930
schemaforge = "schemaforge.cli:main"

src/schemaforge/cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .diff import diff_schemas
1111
from .type_config import TypeConfig
1212
from .check import check_directory
13+
from .mcp_server import mcp_command
1314

1415
# All supported format names (used for CLI choices and detection)
1516
_FORMATS = ["sql", "prisma", "drizzle", "typeorm", "django", "sqlalchemy", "alembic", "json_schema", "graphql"]
@@ -118,6 +119,10 @@ def check(directory: str, canonical: str, type_map_path: str | None) -> None:
118119
sys.exit(1)
119120

120121

122+
# Register the MCP server subcommand
123+
main.add_command(mcp_command)
124+
125+
121126
def _detect_format(path: str) -> str:
122127
ext = Path(path).suffix.lower()
123128
if ext == ".sql":

src/schemaforge/mcp_server.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
"""MCP server for SchemaForge — exposes schema operations as AI-usable tools.
2+
3+
Run with:
4+
schemaforge mcp # stdio transport (default for AI clients)
5+
schemaforge mcp --sse # SSE transport (HTTP server)
6+
"""
7+
from __future__ import annotations
8+
9+
import sys
10+
from pathlib import Path
11+
from typing import Any
12+
13+
import click
14+
15+
from .convert import convert_schema
16+
from .diff import diff_schemas
17+
from .check import check_directory, detect_format
18+
from .type_config import TypeConfig
19+
20+
# Try to import mcp — soft dependency
21+
try:
22+
from mcp.server.fastmcp import FastMCP
23+
except ImportError:
24+
FastMCP = None # type: ignore
25+
26+
27+
# All supported formats
28+
_FORMATS = ["sql", "prisma", "drizzle", "typeorm", "django", "sqlalchemy", "alembic", "json_schema", "graphql"]
29+
_FORMAT_DESCRIPTIONS = {
30+
"sql": "SQL DDL (Data Definition Language)",
31+
"prisma": "Prisma schema",
32+
"drizzle": "Drizzle ORM schema (TypeScript)",
33+
"typeorm": "TypeORM entity decorators (TypeScript)",
34+
"django": "Django models (Python)",
35+
"sqlalchemy": "SQLAlchemy declarative models (Python)",
36+
"alembic": "Alembic migration scripts (Python, generator-only)",
37+
"json_schema": "JSON Schema (draft 2020-12)",
38+
"graphql": "GraphQL SDL (Schema Definition Language)",
39+
}
40+
41+
42+
def create_server() -> Any:
43+
"""Create and configure the MCP server with all SchemaForge tools."""
44+
if FastMCP is None:
45+
raise ImportError(
46+
"The 'mcp' package is required to run the MCP server.\n"
47+
"Install it with: pip install mcp"
48+
)
49+
50+
server = FastMCP("SchemaForge", log_level="WARNING")
51+
52+
@server.tool(
53+
name="convert",
54+
description="Convert a schema from one format to another. "
55+
"All 9 formats support conversion to and from every other format. "
56+
"Returns the converted schema as text.",
57+
)
58+
def convert_tool(
59+
schema_text: str,
60+
from_format: str = "sql",
61+
to_format: str = "prisma",
62+
type_map_path: str | None = None,
63+
) -> str:
64+
"""Convert a schema between formats.
65+
66+
Args:
67+
schema_text: The schema text to convert.
68+
from_format: Source format (sql, prisma, drizzle, typeorm, django,
69+
sqlalchemy, alembic, json_schema, graphql).
70+
to_format: Target format (same options as from_format).
71+
type_map_path: Optional path to a YAML/JSON type mapping config file.
72+
"""
73+
if from_format not in _FORMATS:
74+
return f"Error: Unsupported source format '{from_format}'. Supported: {', '.join(_FORMATS)}"
75+
if to_format not in _FORMATS:
76+
return f"Error: Unsupported target format '{to_format}'. Supported: {', '.join(_FORMATS)}"
77+
78+
type_config: TypeConfig | None = None
79+
if type_map_path:
80+
try:
81+
type_config = TypeConfig.from_file(type_map_path)
82+
except (FileNotFoundError, ValueError) as e:
83+
return f"Error loading type map: {e}"
84+
85+
try:
86+
result = convert_schema(schema_text, from_format, to_format, type_config=type_config)
87+
return result
88+
except ValueError as e:
89+
return f"Error: {e}"
90+
except NotImplementedError:
91+
return f"Error: {from_format}{to_format} conversion is not supported (Alembic is generator-only)."
92+
except Exception as e:
93+
return f"Error: {e}"
94+
95+
@server.tool(
96+
name="diff",
97+
description="Compare two schemas in the same format and return differences. "
98+
"Detects added, removed, and modified tables, columns, indexes, and constraints.",
99+
)
100+
def diff_tool(
101+
schema_a: str,
102+
schema_b: str,
103+
format: str = "sql",
104+
) -> str:
105+
"""Compare two schemas and show differences.
106+
107+
Args:
108+
schema_a: First schema text.
109+
schema_b: Second schema text to compare against.
110+
format: Schema format (sql, prisma, drizzle, typeorm, django,
111+
sqlalchemy, json_schema, graphql). Default: sql.
112+
"""
113+
if format not in _FORMATS:
114+
return f"Error: Unsupported format '{format}'. Supported: {', '.join(_FORMATS)}"
115+
116+
try:
117+
result = diff_schemas(schema_a, schema_b, format)
118+
return result if result.strip() else "No differences found — schemas are equivalent."
119+
except Exception as e:
120+
return f"Error: {e}"
121+
122+
@server.tool(
123+
name="check",
124+
description="Verify all schema files in a directory produce equivalent schemas. "
125+
"Useful for CI/CD to ensure consistency across format representations.",
126+
)
127+
def check_tool(
128+
directory: str,
129+
canonical: str = "sql",
130+
type_map_path: str | None = None,
131+
) -> str:
132+
"""Check schema consistency in a directory.
133+
134+
Args:
135+
directory: Path to directory containing schema files.
136+
canonical: Canonical format for comparison (default: sql).
137+
type_map_path: Optional path to a YAML/JSON type mapping config file.
138+
"""
139+
try:
140+
result = check_directory(directory, canonical=canonical, type_map_path=type_map_path)
141+
return result
142+
except NotADirectoryError as e:
143+
return f"Error: {e}"
144+
except Exception as e:
145+
return f"Error: {e}"
146+
147+
@server.tool(
148+
name="formats",
149+
description="List all supported schema formats with their descriptions. "
150+
"Returns the list of format identifiers and what they represent.",
151+
)
152+
def formats_tool() -> str:
153+
"""List all supported schema formats."""
154+
lines = ["Supported SchemaForge formats:", ""]
155+
for fmt in _FORMATS:
156+
desc = _FORMAT_DESCRIPTIONS.get(fmt, "")
157+
bidirectional = "✓" if fmt != "alembic" else "— (generator only)"
158+
lines.append(f" {fmt:15s} {desc}")
159+
lines.append(f" {'':15s} Bidirectional: {bidirectional}")
160+
lines.append("")
161+
return "\n".join(lines)
162+
163+
@server.tool(
164+
name="detect_format",
165+
description="Detect the schema format from a filename or file extension. "
166+
"Returns the format identifier or 'unknown' if not recognized.",
167+
)
168+
def detect_format_tool(filename: str) -> str:
169+
"""Detect schema format from filename.
170+
171+
Args:
172+
filename: Name or path of the schema file.
173+
"""
174+
fmt = detect_format(filename)
175+
if fmt:
176+
return fmt
177+
# Also try by extension
178+
ext = Path(filename).suffix.lower()
179+
ext_map = {
180+
".sql": "sql",
181+
".prisma": "prisma",
182+
".ts": "drizzle",
183+
".tsx": "drizzle",
184+
".py": "django",
185+
".json": "json_schema",
186+
".graphql": "graphql",
187+
".gql": "graphql",
188+
}
189+
fmt = ext_map.get(ext)
190+
return fmt if fmt else f"unknown (extension: {ext})"
191+
192+
return server
193+
194+
195+
@click.command(name="mcp")
196+
@click.option("--sse", is_flag=True, help="Run as SSE HTTP server instead of stdio")
197+
@click.option("--host", default="127.0.0.1", help="SSE host (default: 127.0.0.1)")
198+
@click.option("--port", default=8000, type=int, help="SSE port (default: 8000)")
199+
def mcp_command(sse: bool, host: str, port: int) -> None:
200+
"""Run SchemaForge as an MCP (Model Context Protocol) server.
201+
202+
By default runs in stdio mode for AI clients (Claude Desktop, Cursor, etc.).
203+
Use --sse for HTTP transport.
204+
"""
205+
server = create_server()
206+
207+
if sse:
208+
click.echo(f"Starting SchemaForge MCP server on http://{host}:{port}", err=True)
209+
server.run(transport="sse", host=host, port=port)
210+
else:
211+
server.run(transport="stdio")

0 commit comments

Comments
 (0)