BoilerWorks

boilerworks / boilerworks / mcp_server.py
Blame History Raw 213 lines
1
"""Boilerworks MCP server — exposes the CLI as tools for AI agents."""
2
3
from __future__ import annotations
4
5
import json
6
from pathlib import Path
7
from typing import Annotated
8
9
from mcp.server.fastmcp import FastMCP
10
11
mcp = FastMCP(
12
"boilerworks",
13
instructions=(
14
"Boilerworks provides 26 production-ready project templates structured for AI-assisted development. "
15
"Use list_templates to explore options, create_manifest to build a boilerworks.yaml, "
16
"and init_project to scaffold the project on disk."
17
),
18
)
19
20
21
# ── Templates ─────────────────────────────────────────────────────────────────
22
23
24
@mcp.tool()
25
def list_templates(
26
size: Annotated[str | None, "Filter by size: full | micro | edge"] = None,
27
language: Annotated[str | None, "Filter by language: python | typescript | ruby | php | java | go | elixir | rust | svelte"] = None,
28
status: Annotated[str | None, "Filter by status: done | building | planned"] = None,
29
) -> str:
30
"""List all available Boilerworks templates, optionally filtered."""
31
from boilerworks.registry import Registry
32
33
registry = Registry()
34
templates = registry.list_all()
35
36
if size:
37
templates = [t for t in templates if t.size == size]
38
if language:
39
templates = [t for t in templates if t.language == language]
40
if status:
41
templates = [t for t in templates if t.status == status]
42
43
rows = [
44
{
45
"name": t.name,
46
"size": t.size,
47
"language": t.language,
48
"backend": t.backend,
49
"frontend": t.frontend,
50
"status": t.status,
51
"best_for": t.best_for,
52
}
53
for t in templates
54
]
55
return json.dumps(rows, indent=2)
56
57
58
@mcp.tool()
59
def get_template(
60
name: Annotated[str, "Template name, e.g. django-nextjs"],
61
) -> str:
62
"""Get full details for a specific Boilerworks template."""
63
from boilerworks.registry import Registry
64
65
registry = Registry()
66
template = registry.get_by_name(name)
67
68
if template is None:
69
valid = ", ".join(sorted(registry.names()))
70
return f"Template '{name}' not found. Available: {valid}"
71
72
return json.dumps(template.model_dump(), indent=2)
73
74
75
@mcp.tool()
76
def search_templates(
77
query: Annotated[str, "Search query matched against name, description, and best_for"],
78
) -> str:
79
"""Search templates by keyword."""
80
from boilerworks.registry import Registry
81
82
registry = Registry()
83
results = registry.search(query)
84
85
if not results:
86
return f"No templates matched '{query}'."
87
88
rows = [{"name": t.name, "size": t.size, "backend": t.backend, "frontend": t.frontend, "best_for": t.best_for} for t in results]
89
return json.dumps(rows, indent=2)
90
91
92
# ── Manifest ──────────────────────────────────────────────────────────────────
93
94
95
@mcp.tool()
96
def create_manifest(
97
project: Annotated[str, "Project slug (lowercase, letters/digits/hyphens)"],
98
family: Annotated[str, "Template family name, e.g. django-nextjs"],
99
size: Annotated[str, "Template size: full | micro | edge"],
100
cloud: Annotated[str | None, "Cloud provider: aws | gcp | azure"] = None,
101
region: Annotated[str | None, "Cloud region, e.g. us-east-1"] = None,
102
topology: Annotated[str, "standard | api-only | omni"] = "standard",
103
domain: Annotated[str | None, "Production domain, e.g. myapp.com"] = None,
104
ops: Annotated[bool, "Include Terraform infrastructure repo"] = False,
105
mobile: Annotated[bool, "Include mobile app template (Full only)"] = False,
106
web_presence: Annotated[bool, "Include marketing site template (Full only)"] = False,
107
compliance: Annotated[list[str] | None, "Compliance requirements: soc2 | hipaa | pci-dss | gdpr"] = None,
108
email: Annotated[str | None, "Email service: ses | sendgrid | mailgun"] = None,
109
storage: Annotated[str | None, "File storage: s3 | gcs | azure-blob"] = None,
110
search: Annotated[str | None, "Search engine: opensearch | meilisearch"] = None,
111
cache: Annotated[str, "Cache: redis | memcached"] = "redis",
112
database: Annotated[str, "Database: postgres | mysql | sqlite"] = "postgres",
113
e2e: Annotated[str | None, "E2E framework: playwright | cypress"] = None,
114
) -> str:
115
"""Build and validate a boilerworks.yaml manifest. Returns the YAML content ready to write to disk."""
116
from boilerworks.manifest import BoilerworksManifest, DataConfig, ServicesConfig, TestingConfig
117
118
try:
119
manifest = BoilerworksManifest(
120
project=project,
121
family=family,
122
size=size,
123
topology=topology,
124
cloud=cloud,
125
ops=ops,
126
region=region,
127
domain=domain,
128
mobile=mobile,
129
web_presence=web_presence,
130
compliance=compliance or [],
131
services=ServicesConfig(
132
email=email,
133
storage=storage,
134
search=search,
135
cache=cache,
136
),
137
data=DataConfig(database=database),
138
testing=TestingConfig(e2e=e2e),
139
)
140
except Exception as exc:
141
return f"Invalid manifest: {exc}"
142
143
return manifest.to_yaml()
144
145
146
@mcp.tool()
147
def validate_manifest(
148
yaml_content: Annotated[str, "Contents of a boilerworks.yaml file to validate"],
149
) -> str:
150
"""Validate a boilerworks.yaml manifest. Returns 'valid' or a description of the errors."""
151
from boilerworks.manifest import BoilerworksManifest
152
153
try:
154
BoilerworksManifest.from_yaml(yaml_content)
155
return "valid"
156
except Exception as exc:
157
return f"invalid: {exc}"
158
159
160
# ── Project generation ─────────────────────────────────────────────────────────
161
162
163
def _run_cli(args: list[str], manifest_yaml: str) -> str:
164
"""Write manifest to a temp file, run the boilerworks CLI, return combined output."""
165
import subprocess
166
import sys
167
import tempfile
168
169
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
170
f.write(manifest_yaml)
171
tmp_path = f.name
172
173
try:
174
result = subprocess.run(
175
[sys.executable, "-m", "boilerworks.cli"] + args + ["--manifest", tmp_path],
176
capture_output=True,
177
text=True,
178
)
179
output = (result.stdout + result.stderr).strip()
180
return output or ("OK" if result.returncode == 0 else "Command failed with no output.")
181
finally:
182
Path(tmp_path).unlink(missing_ok=True)
183
184
185
@mcp.tool()
186
def dry_run(
187
manifest_yaml: Annotated[str, "Contents of a boilerworks.yaml manifest"],
188
output_dir: Annotated[str, "Directory where the project would be created"] = ".",
189
) -> str:
190
"""Preview what boilerworks init would do without writing any files."""
191
return _run_cli(["init", "--dry-run", "--output", output_dir], manifest_yaml)
192
193
194
@mcp.tool()
195
def init_project(
196
manifest_yaml: Annotated[str, "Contents of a boilerworks.yaml manifest"],
197
output_dir: Annotated[str, "Directory to generate the project in"] = ".",
198
) -> str:
199
"""
200
Scaffold a new project from a boilerworks.yaml manifest.
201
Clones the template, applies substitutions, and runs git init.
202
Requires network access (GitHub). This may take 10-30 seconds.
203
"""
204
return _run_cli(["init", "--output", output_dir], manifest_yaml)
205
206
207
def main() -> None:
208
mcp.run()
209
210
211
if __name__ == "__main__":
212
main()
213

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button