mcp-creator-python 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_creator/__init__.py +1 -0
- mcp_creator/server.py +202 -0
- mcp_creator/services/__init__.py +0 -0
- mcp_creator/services/codegen.py +593 -0
- mcp_creator/services/file_writer.py +39 -0
- mcp_creator/services/pypi_client.py +55 -0
- mcp_creator/services/subprocess_runner.py +51 -0
- mcp_creator/tools/__init__.py +0 -0
- mcp_creator/tools/add_tool.py +94 -0
- mcp_creator/tools/build_package.py +50 -0
- mcp_creator/tools/check_pypi_name.py +37 -0
- mcp_creator/tools/check_setup.py +104 -0
- mcp_creator/tools/creator_profile.py +106 -0
- mcp_creator/tools/generate_launchguide.py +95 -0
- mcp_creator/tools/publish_package.py +72 -0
- mcp_creator/tools/scaffold_server.py +122 -0
- mcp_creator/tools/setup_github.py +137 -0
- mcp_creator/transport.py +11 -0
- mcp_creator_python-1.0.0.dist-info/METADATA +117 -0
- mcp_creator_python-1.0.0.dist-info/RECORD +22 -0
- mcp_creator_python-1.0.0.dist-info/WHEEL +4 -0
- mcp_creator_python-1.0.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
"""Code generation for scaffolded MCP server projects.
|
|
2
|
+
|
|
3
|
+
All functions are pure — they return strings, no I/O.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
PYPROJECT_TEMPLATE = """\
|
|
9
|
+
[project]
|
|
10
|
+
name = "{package_name}"
|
|
11
|
+
version = "0.1.0"
|
|
12
|
+
description = "{description}"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
|
+
license = {{ text = "MIT" }}
|
|
16
|
+
dependencies = [
|
|
17
|
+
"mcp[cli]>=1.0.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
{package_name} = "{module_name}.server:main"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["hatchling"]
|
|
25
|
+
build-backend = "hatchling.build"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/{module_name}"]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.0.0",
|
|
33
|
+
"pytest-asyncio>=0.23.0",
|
|
34
|
+
]
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
GITIGNORE_TEMPLATE = """\
|
|
38
|
+
__pycache__/
|
|
39
|
+
*.py[cod]
|
|
40
|
+
*$py.class
|
|
41
|
+
*.egg-info/
|
|
42
|
+
dist/
|
|
43
|
+
build/
|
|
44
|
+
.eggs/
|
|
45
|
+
*.egg
|
|
46
|
+
.venv/
|
|
47
|
+
venv/
|
|
48
|
+
.env
|
|
49
|
+
*.so
|
|
50
|
+
.pytest_cache/
|
|
51
|
+
.mypy_cache/
|
|
52
|
+
.ruff_cache/
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
INIT_TEMPLATE = '""""{package_name} MCP server."""\n'
|
|
56
|
+
|
|
57
|
+
TRANSPORT_TEMPLATE = """\
|
|
58
|
+
\"\"\"Transport helpers for {package_name}.\"\"\"
|
|
59
|
+
|
|
60
|
+
import sys
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_stdio(mcp_app):
|
|
64
|
+
\"\"\"Run the MCP server over stdio (default for Claude Code / Cursor).\"\"\"
|
|
65
|
+
mcp_app.run(transport="stdio")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run_http(mcp_app, host: str = "0.0.0.0", port: int = 8000):
|
|
69
|
+
\"\"\"Run the MCP server over HTTP (for remote hosting).\"\"\"
|
|
70
|
+
mcp_app.run(transport="sse", host=host, port=port)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _to_module_name(package_name: str) -> str:
|
|
75
|
+
"""Convert a PyPI package name to a Python module name."""
|
|
76
|
+
return package_name.replace("-", "_")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _python_type(type_str: str) -> str:
|
|
80
|
+
"""Map a simple type string to a Python type annotation."""
|
|
81
|
+
mapping = {
|
|
82
|
+
"string": "str",
|
|
83
|
+
"str": "str",
|
|
84
|
+
"integer": "int",
|
|
85
|
+
"int": "int",
|
|
86
|
+
"number": "float",
|
|
87
|
+
"float": "float",
|
|
88
|
+
"boolean": "bool",
|
|
89
|
+
"bool": "bool",
|
|
90
|
+
"list": "list",
|
|
91
|
+
"array": "list",
|
|
92
|
+
"dict": "dict",
|
|
93
|
+
"object": "dict",
|
|
94
|
+
}
|
|
95
|
+
return mapping.get(type_str.lower(), "str")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def render_pyproject(package_name: str, description: str, *, paid: bool = False) -> str:
|
|
99
|
+
module_name = _to_module_name(package_name)
|
|
100
|
+
base = PYPROJECT_TEMPLATE.format(
|
|
101
|
+
package_name=package_name,
|
|
102
|
+
description=description,
|
|
103
|
+
module_name=module_name,
|
|
104
|
+
)
|
|
105
|
+
if paid:
|
|
106
|
+
base = base.replace(
|
|
107
|
+
' "mcp[cli]>=1.0.0",\n]',
|
|
108
|
+
' "mcp[cli]>=1.0.0",\n "mcp-marketplace-license>=1.1.0",\n]',
|
|
109
|
+
)
|
|
110
|
+
return base
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def render_gitignore() -> str:
|
|
114
|
+
return GITIGNORE_TEMPLATE
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def render_init(package_name: str) -> str:
|
|
118
|
+
return INIT_TEMPLATE.format(package_name=package_name)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def render_transport(package_name: str) -> str:
|
|
122
|
+
return TRANSPORT_TEMPLATE.format(package_name=package_name)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def render_env_example(
|
|
126
|
+
env_vars: list[dict] | None,
|
|
127
|
+
*,
|
|
128
|
+
paid: bool = False,
|
|
129
|
+
hosting: str = "local",
|
|
130
|
+
) -> str | None:
|
|
131
|
+
"""Render .env.example if env vars are declared or paid/remote. Returns None if nothing needed."""
|
|
132
|
+
has_vars = bool(env_vars) or paid or hosting == "remote"
|
|
133
|
+
if not has_vars:
|
|
134
|
+
return None
|
|
135
|
+
lines = ["# Environment variables for this MCP server", ""]
|
|
136
|
+
if paid:
|
|
137
|
+
lines.append("# License key for paid features (required)")
|
|
138
|
+
lines.append("# Get one at mcp-marketplace.io")
|
|
139
|
+
lines.append("MCP_LICENSE_KEY=")
|
|
140
|
+
lines.append("")
|
|
141
|
+
if hosting == "remote":
|
|
142
|
+
lines.append("# Server port (optional, default 8000)")
|
|
143
|
+
lines.append("PORT=8000")
|
|
144
|
+
lines.append("")
|
|
145
|
+
if env_vars:
|
|
146
|
+
for var in env_vars:
|
|
147
|
+
name = var.get("name", "UNKNOWN")
|
|
148
|
+
desc = var.get("description", "")
|
|
149
|
+
required = var.get("required", True)
|
|
150
|
+
tag = "required" if required else "optional"
|
|
151
|
+
lines.append(f"# {desc} ({tag})")
|
|
152
|
+
lines.append(f"{name}=")
|
|
153
|
+
lines.append("")
|
|
154
|
+
return "\n".join(lines)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def render_server(
|
|
158
|
+
package_name: str,
|
|
159
|
+
tools: list[dict],
|
|
160
|
+
*,
|
|
161
|
+
paid: bool = False,
|
|
162
|
+
paid_tools: list[str] | None = None,
|
|
163
|
+
hosting: str = "local",
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Render the main server.py with FastMCP and tool registrations."""
|
|
166
|
+
module_name = _to_module_name(package_name)
|
|
167
|
+
gated = set(paid_tools or [])
|
|
168
|
+
|
|
169
|
+
lines = [
|
|
170
|
+
f'"""MCP server for {package_name}."""',
|
|
171
|
+
"",
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
if paid:
|
|
175
|
+
lines.append("import json")
|
|
176
|
+
lines.append("")
|
|
177
|
+
|
|
178
|
+
if hosting == "remote":
|
|
179
|
+
lines.append("import os")
|
|
180
|
+
lines.append("")
|
|
181
|
+
|
|
182
|
+
lines.append("from mcp.server.fastmcp import FastMCP")
|
|
183
|
+
|
|
184
|
+
if paid:
|
|
185
|
+
lines.append("from mcp_marketplace_license import verify_license")
|
|
186
|
+
|
|
187
|
+
lines.append("")
|
|
188
|
+
lines.append("# --- IMPORTS ---")
|
|
189
|
+
|
|
190
|
+
for tool in tools:
|
|
191
|
+
tool_name = tool["name"]
|
|
192
|
+
lines.append(
|
|
193
|
+
f"from {module_name}.tools.{tool_name} import {tool_name} as _{tool_name}_impl"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
lines.append("# --- END IMPORTS ---")
|
|
197
|
+
lines.append("")
|
|
198
|
+
lines.append(f'mcp = FastMCP("{package_name}")')
|
|
199
|
+
|
|
200
|
+
if paid:
|
|
201
|
+
lines.append("")
|
|
202
|
+
lines.append("")
|
|
203
|
+
lines.append("def _require_license(tool_name: str) -> str | None:")
|
|
204
|
+
lines.append(' """Return None if licensed, or a JSON error string."""')
|
|
205
|
+
lines.append(f' result = verify_license(slug="{package_name}")')
|
|
206
|
+
lines.append(' if result.get("valid"):')
|
|
207
|
+
lines.append(" return None")
|
|
208
|
+
lines.append(" return json.dumps({")
|
|
209
|
+
lines.append(' "error": "premium_required",')
|
|
210
|
+
lines.append(' "reason": result.get("reason", "unknown"),')
|
|
211
|
+
lines.append(" \"message\": f\"The '{tool_name}' tool requires a license. \"")
|
|
212
|
+
lines.append(f' "Set MCP_LICENSE_KEY to unlock it. "')
|
|
213
|
+
lines.append(f' "Get your key at https://mcp-marketplace.io/server/{package_name}",')
|
|
214
|
+
lines.append(" })")
|
|
215
|
+
|
|
216
|
+
lines.append("")
|
|
217
|
+
lines.append("# --- TOOLS ---")
|
|
218
|
+
|
|
219
|
+
for tool in tools:
|
|
220
|
+
tool_name = tool["name"]
|
|
221
|
+
tool_desc = tool.get("description", f"{tool_name} tool")
|
|
222
|
+
params = tool.get("parameters", [])
|
|
223
|
+
is_gated = paid and (not gated or tool_name in gated)
|
|
224
|
+
|
|
225
|
+
# Build parameter list
|
|
226
|
+
param_parts = []
|
|
227
|
+
for p in params:
|
|
228
|
+
pname = p["name"]
|
|
229
|
+
ptype = _python_type(p.get("type", "string"))
|
|
230
|
+
if p.get("required", True):
|
|
231
|
+
param_parts.append(f"{pname}: {ptype}")
|
|
232
|
+
else:
|
|
233
|
+
default = p.get("default")
|
|
234
|
+
if default is None:
|
|
235
|
+
default_str = "None"
|
|
236
|
+
ptype = f"{ptype} | None"
|
|
237
|
+
elif isinstance(default, str):
|
|
238
|
+
default_str = f'"{default}"'
|
|
239
|
+
else:
|
|
240
|
+
default_str = str(default)
|
|
241
|
+
param_parts.append(f"{pname}: {ptype} = {default_str}")
|
|
242
|
+
|
|
243
|
+
param_str = ", ".join(param_parts)
|
|
244
|
+
call_args = ", ".join(p["name"] for p in params)
|
|
245
|
+
|
|
246
|
+
lines.append("")
|
|
247
|
+
lines.append(f'@mcp.tool(description="{tool_desc}")')
|
|
248
|
+
lines.append(f"def {tool_name}({param_str}) -> str:")
|
|
249
|
+
lines.append(f' """Call the {tool_name} tool."""')
|
|
250
|
+
if is_gated:
|
|
251
|
+
lines.append(f' err = _require_license("{tool_name}")')
|
|
252
|
+
lines.append(" if err:")
|
|
253
|
+
lines.append(" return err")
|
|
254
|
+
lines.append(f" return _{tool_name}_impl({call_args})")
|
|
255
|
+
|
|
256
|
+
lines.append("# --- END TOOLS ---")
|
|
257
|
+
lines.append("")
|
|
258
|
+
lines.append("")
|
|
259
|
+
lines.append("def main():")
|
|
260
|
+
lines.append(' """Run the MCP server."""')
|
|
261
|
+
if hosting == "remote":
|
|
262
|
+
lines.append(' port = int(os.environ.get("PORT", "8000"))')
|
|
263
|
+
lines.append(' mcp.run(transport="sse", host="0.0.0.0", port=port)')
|
|
264
|
+
else:
|
|
265
|
+
lines.append(" mcp.run()")
|
|
266
|
+
lines.append("")
|
|
267
|
+
lines.append("")
|
|
268
|
+
lines.append('if __name__ == "__main__":')
|
|
269
|
+
lines.append(" main()")
|
|
270
|
+
lines.append("")
|
|
271
|
+
|
|
272
|
+
return "\n".join(lines)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def render_tool_module(package_name: str, tool: dict) -> str:
|
|
276
|
+
"""Render a single tool module file (tools/<name>.py)."""
|
|
277
|
+
module_name = _to_module_name(package_name)
|
|
278
|
+
tool_name = tool["name"]
|
|
279
|
+
tool_desc = tool.get("description", f"{tool_name} tool")
|
|
280
|
+
params = tool.get("parameters", [])
|
|
281
|
+
returns = tool.get("returns", "Result as JSON string")
|
|
282
|
+
|
|
283
|
+
# Build function signature
|
|
284
|
+
param_parts = []
|
|
285
|
+
for p in params:
|
|
286
|
+
pname = p["name"]
|
|
287
|
+
ptype = _python_type(p.get("type", "string"))
|
|
288
|
+
if p.get("required", True):
|
|
289
|
+
param_parts.append(f"{pname}: {ptype}")
|
|
290
|
+
else:
|
|
291
|
+
default = p.get("default")
|
|
292
|
+
if default is None:
|
|
293
|
+
default_str = "None"
|
|
294
|
+
ptype = f"{ptype} | None"
|
|
295
|
+
elif isinstance(default, str):
|
|
296
|
+
default_str = f'"{default}"'
|
|
297
|
+
else:
|
|
298
|
+
default_str = str(default)
|
|
299
|
+
param_parts.append(f"{pname}: {ptype} = {default_str}")
|
|
300
|
+
|
|
301
|
+
param_str = ", ".join(param_parts)
|
|
302
|
+
|
|
303
|
+
lines = [
|
|
304
|
+
f'"""{tool_desc}."""',
|
|
305
|
+
"",
|
|
306
|
+
"import json",
|
|
307
|
+
"",
|
|
308
|
+
f"from {module_name}.services.{tool_name}_service import {_to_class_name(tool_name)}",
|
|
309
|
+
"",
|
|
310
|
+
"",
|
|
311
|
+
f"def {tool_name}({param_str}) -> str:",
|
|
312
|
+
f' """{tool_desc}',
|
|
313
|
+
"",
|
|
314
|
+
f" Returns:",
|
|
315
|
+
f" {returns}",
|
|
316
|
+
' """',
|
|
317
|
+
f" service = {_to_class_name(tool_name)}()",
|
|
318
|
+
f" result = service.execute({', '.join(p['name'] + '=' + p['name'] for p in params)})",
|
|
319
|
+
" return json.dumps(result, indent=2)",
|
|
320
|
+
"",
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
return "\n".join(lines)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def render_service_module(tool: dict) -> str:
|
|
327
|
+
"""Render a service stub (services/<name>_service.py)."""
|
|
328
|
+
tool_name = tool["name"]
|
|
329
|
+
tool_desc = tool.get("description", f"{tool_name} service")
|
|
330
|
+
params = tool.get("parameters", [])
|
|
331
|
+
class_name = _to_class_name(tool_name)
|
|
332
|
+
|
|
333
|
+
param_parts = []
|
|
334
|
+
for p in params:
|
|
335
|
+
pname = p["name"]
|
|
336
|
+
ptype = _python_type(p.get("type", "string"))
|
|
337
|
+
if p.get("required", True):
|
|
338
|
+
param_parts.append(f"{pname}: {ptype}")
|
|
339
|
+
else:
|
|
340
|
+
default = p.get("default")
|
|
341
|
+
if default is None:
|
|
342
|
+
default_str = "None"
|
|
343
|
+
ptype = f"{ptype} | None"
|
|
344
|
+
elif isinstance(default, str):
|
|
345
|
+
default_str = f'"{default}"'
|
|
346
|
+
else:
|
|
347
|
+
default_str = str(default)
|
|
348
|
+
param_parts.append(f"{pname}: {ptype} = {default_str}")
|
|
349
|
+
|
|
350
|
+
param_str = ", ".join(param_parts)
|
|
351
|
+
|
|
352
|
+
# Build placeholder return dict
|
|
353
|
+
placeholder_fields = {}
|
|
354
|
+
for p in params:
|
|
355
|
+
placeholder_fields[p["name"]] = p["name"]
|
|
356
|
+
placeholder_fields["status"] = '"ok"'
|
|
357
|
+
|
|
358
|
+
result_lines = [f' "{k}": {v},' for k, v in placeholder_fields.items()]
|
|
359
|
+
result_block = "\n".join(result_lines)
|
|
360
|
+
|
|
361
|
+
lines = [
|
|
362
|
+
f'"""{tool_desc} — service layer."""',
|
|
363
|
+
"",
|
|
364
|
+
"",
|
|
365
|
+
f"class {class_name}:",
|
|
366
|
+
f' """{tool_desc}.',
|
|
367
|
+
"",
|
|
368
|
+
" TODO: Replace the stub implementation with your real logic.",
|
|
369
|
+
' """',
|
|
370
|
+
"",
|
|
371
|
+
f" def execute(self, {param_str}) -> dict:",
|
|
372
|
+
f' """Run {tool_name} and return results."""',
|
|
373
|
+
" # TODO: Implement your logic here",
|
|
374
|
+
" return {",
|
|
375
|
+
result_block,
|
|
376
|
+
" }",
|
|
377
|
+
"",
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
return "\n".join(lines)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def render_test_server(package_name: str, tools: list[dict]) -> str:
|
|
384
|
+
"""Render test_server.py that verifies tool registration."""
|
|
385
|
+
module_name = _to_module_name(package_name)
|
|
386
|
+
tool_names = [t["name"] for t in tools]
|
|
387
|
+
expected_set = "{" + ", ".join(f'"{n}"' for n in tool_names) + "}"
|
|
388
|
+
|
|
389
|
+
lines = [
|
|
390
|
+
f'"""Test that all tools are registered on the MCP server."""',
|
|
391
|
+
"",
|
|
392
|
+
f"from {module_name}.server import mcp",
|
|
393
|
+
"",
|
|
394
|
+
"",
|
|
395
|
+
"def test_tools_registered():",
|
|
396
|
+
f" tool_names = set(mcp._tool_manager._tools.keys())",
|
|
397
|
+
f" expected = {expected_set}",
|
|
398
|
+
" assert expected.issubset(tool_names), f\"Missing tools: {expected - tool_names}\"",
|
|
399
|
+
"",
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
return "\n".join(lines)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def render_test_tool(package_name: str, tool: dict) -> str:
|
|
406
|
+
"""Render a basic test for a single tool."""
|
|
407
|
+
module_name = _to_module_name(package_name)
|
|
408
|
+
tool_name = tool["name"]
|
|
409
|
+
params = tool.get("parameters", [])
|
|
410
|
+
|
|
411
|
+
# Build test call args
|
|
412
|
+
test_args = []
|
|
413
|
+
for p in params:
|
|
414
|
+
ptype = p.get("type", "string").lower()
|
|
415
|
+
if ptype in ("string", "str"):
|
|
416
|
+
test_args.append(f'{p["name"]}="test"')
|
|
417
|
+
elif ptype in ("integer", "int"):
|
|
418
|
+
test_args.append(f'{p["name"]}=1')
|
|
419
|
+
elif ptype in ("number", "float"):
|
|
420
|
+
test_args.append(f'{p["name"]}=1.0')
|
|
421
|
+
elif ptype in ("boolean", "bool"):
|
|
422
|
+
test_args.append(f'{p["name"]}=True')
|
|
423
|
+
else:
|
|
424
|
+
test_args.append(f'{p["name"]}="test"')
|
|
425
|
+
|
|
426
|
+
args_str = ", ".join(test_args)
|
|
427
|
+
|
|
428
|
+
lines = [
|
|
429
|
+
f'"""Test {tool_name} tool."""',
|
|
430
|
+
"",
|
|
431
|
+
"import json",
|
|
432
|
+
"",
|
|
433
|
+
f"from {module_name}.tools.{tool_name} import {tool_name}",
|
|
434
|
+
"",
|
|
435
|
+
"",
|
|
436
|
+
f"def test_{tool_name}_returns_json():",
|
|
437
|
+
f" result = {tool_name}({args_str})",
|
|
438
|
+
" data = json.loads(result)",
|
|
439
|
+
" assert isinstance(data, dict)",
|
|
440
|
+
"",
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
return "\n".join(lines)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def render_readme(
|
|
447
|
+
package_name: str,
|
|
448
|
+
description: str,
|
|
449
|
+
tools: list[dict],
|
|
450
|
+
*,
|
|
451
|
+
paid: bool = False,
|
|
452
|
+
hosting: str = "local",
|
|
453
|
+
) -> str:
|
|
454
|
+
"""Render README.md for the generated project."""
|
|
455
|
+
module_name = _to_module_name(package_name)
|
|
456
|
+
|
|
457
|
+
tool_list = "\n".join(
|
|
458
|
+
f"- **{t['name']}** — {t.get('description', t['name'])}" for t in tools
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
sections = [f"# {package_name}", "", description, ""]
|
|
462
|
+
|
|
463
|
+
if hosting == "local":
|
|
464
|
+
sections += [
|
|
465
|
+
"## Install", "",
|
|
466
|
+
"```bash", f"uvx {package_name}", "```", "",
|
|
467
|
+
"Or install permanently:", "",
|
|
468
|
+
"```bash", f"pip install {package_name}", "```", "",
|
|
469
|
+
]
|
|
470
|
+
else:
|
|
471
|
+
sections += [
|
|
472
|
+
"## Connect", "",
|
|
473
|
+
"This is a remote MCP server. Add to your Claude Code config:", "",
|
|
474
|
+
"```json", "{", f' "mcpServers": {{', f' "{package_name}": {{',
|
|
475
|
+
f' "url": "https://your-server.com/mcp"',
|
|
476
|
+
" }", " }", "}", "```", "",
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
sections += ["## Tools", "", tool_list, ""]
|
|
480
|
+
|
|
481
|
+
if paid:
|
|
482
|
+
sections += [
|
|
483
|
+
"## License Key", "",
|
|
484
|
+
f"This server requires a license key from [MCP Marketplace](https://mcp-marketplace.io/server/{package_name}).", "",
|
|
485
|
+
"Set the `MCP_LICENSE_KEY` environment variable:", "",
|
|
486
|
+
"```bash", "export MCP_LICENSE_KEY=mcp_live_your_key_here", "```", "",
|
|
487
|
+
]
|
|
488
|
+
|
|
489
|
+
if hosting == "local":
|
|
490
|
+
sections += [
|
|
491
|
+
"## Usage with Claude Code", "",
|
|
492
|
+
"```bash",
|
|
493
|
+
f'claude mcp add {package_name} -- uvx {package_name}',
|
|
494
|
+
"```", "",
|
|
495
|
+
"Or add to your MCP config (`~/.claude/settings.json`):", "",
|
|
496
|
+
"```json", "{{", ' "mcpServers": {{',
|
|
497
|
+
f' "{package_name}": {{',
|
|
498
|
+
f' "command": "uvx",',
|
|
499
|
+
f' "args": ["{package_name}"]',
|
|
500
|
+
]
|
|
501
|
+
if paid:
|
|
502
|
+
sections[-1] = f' "args": ["{package_name}"],'
|
|
503
|
+
sections += [
|
|
504
|
+
f' "env": {{ "MCP_LICENSE_KEY": "mcp_live_your_key_here" }}',
|
|
505
|
+
]
|
|
506
|
+
sections += [" }}", " }}", "}}", "```", ""]
|
|
507
|
+
elif hosting == "remote":
|
|
508
|
+
sections += [
|
|
509
|
+
"## Deployment", "",
|
|
510
|
+
"```bash", "docker build -t " + package_name + " .",
|
|
511
|
+
"docker run -p 8000:8000 " + package_name, "```", "",
|
|
512
|
+
]
|
|
513
|
+
|
|
514
|
+
sections += [
|
|
515
|
+
"## Development", "",
|
|
516
|
+
"```bash",
|
|
517
|
+
f"git clone https://github.com/YOUR_USERNAME/{package_name}.git",
|
|
518
|
+
f"cd {package_name}",
|
|
519
|
+
"uv venv .venv && source .venv/bin/activate",
|
|
520
|
+
'uv pip install -e ".[dev]"',
|
|
521
|
+
"pytest -v",
|
|
522
|
+
"```",
|
|
523
|
+
"",
|
|
524
|
+
]
|
|
525
|
+
|
|
526
|
+
return "\n".join(sections)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _to_class_name(snake_name: str) -> str:
|
|
530
|
+
"""Convert snake_case to PascalCase."""
|
|
531
|
+
return "".join(word.capitalize() for word in snake_name.split("_"))
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def render_dockerfile(package_name: str) -> str:
|
|
535
|
+
"""Render a Dockerfile for remote hosting."""
|
|
536
|
+
return f"""\
|
|
537
|
+
FROM python:3.11-slim
|
|
538
|
+
|
|
539
|
+
WORKDIR /app
|
|
540
|
+
|
|
541
|
+
COPY . .
|
|
542
|
+
|
|
543
|
+
RUN pip install --no-cache-dir .
|
|
544
|
+
|
|
545
|
+
ENV PORT=8000
|
|
546
|
+
|
|
547
|
+
EXPOSE ${{PORT}}
|
|
548
|
+
|
|
549
|
+
CMD ["{package_name}"]
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def render_add_tool_import(package_name: str, tool_name: str) -> str:
|
|
554
|
+
"""Render the import line for a new tool to inject into server.py."""
|
|
555
|
+
module_name = _to_module_name(package_name)
|
|
556
|
+
return f"from {module_name}.tools.{tool_name} import {tool_name} as _{tool_name}_impl"
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def render_add_tool_registration(tool: dict) -> str:
|
|
560
|
+
"""Render the @mcp.tool decorated function for a new tool."""
|
|
561
|
+
tool_name = tool["name"]
|
|
562
|
+
tool_desc = tool.get("description", f"{tool_name} tool")
|
|
563
|
+
params = tool.get("parameters", [])
|
|
564
|
+
|
|
565
|
+
param_parts = []
|
|
566
|
+
for p in params:
|
|
567
|
+
pname = p["name"]
|
|
568
|
+
ptype = _python_type(p.get("type", "string"))
|
|
569
|
+
if p.get("required", True):
|
|
570
|
+
param_parts.append(f"{pname}: {ptype}")
|
|
571
|
+
else:
|
|
572
|
+
default = p.get("default")
|
|
573
|
+
if default is None:
|
|
574
|
+
default_str = "None"
|
|
575
|
+
ptype = f"{ptype} | None"
|
|
576
|
+
elif isinstance(default, str):
|
|
577
|
+
default_str = f'"{default}"'
|
|
578
|
+
else:
|
|
579
|
+
default_str = str(default)
|
|
580
|
+
param_parts.append(f"{pname}: {ptype} = {default_str}")
|
|
581
|
+
|
|
582
|
+
param_str = ", ".join(param_parts)
|
|
583
|
+
call_args = ", ".join(p["name"] for p in params)
|
|
584
|
+
|
|
585
|
+
lines = [
|
|
586
|
+
"",
|
|
587
|
+
f'@mcp.tool(description="{tool_desc}")',
|
|
588
|
+
f"def {tool_name}({param_str}) -> str:",
|
|
589
|
+
f' """Call the {tool_name} tool."""',
|
|
590
|
+
f" return _{tool_name}_impl({call_args})",
|
|
591
|
+
]
|
|
592
|
+
|
|
593
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Write generated files to disk."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def write_project_files(base_dir: str | Path, files: dict[str, str]) -> list[str]:
|
|
9
|
+
"""Write a dict of {relative_path: content} to base_dir.
|
|
10
|
+
|
|
11
|
+
Creates parent directories as needed.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
List of absolute paths written.
|
|
15
|
+
"""
|
|
16
|
+
base = Path(base_dir)
|
|
17
|
+
written = []
|
|
18
|
+
for rel_path, content in files.items():
|
|
19
|
+
full_path = base / rel_path
|
|
20
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
full_path.write_text(content, encoding="utf-8")
|
|
22
|
+
written.append(str(full_path))
|
|
23
|
+
return written
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def inject_after_sentinel(
|
|
27
|
+
file_path: str | Path, sentinel: str, content: str
|
|
28
|
+
) -> bool:
|
|
29
|
+
"""Insert content after a sentinel comment line in a file.
|
|
30
|
+
|
|
31
|
+
Returns True if injection succeeded, False if sentinel not found.
|
|
32
|
+
"""
|
|
33
|
+
path = Path(file_path)
|
|
34
|
+
text = path.read_text(encoding="utf-8")
|
|
35
|
+
if sentinel not in text:
|
|
36
|
+
return False
|
|
37
|
+
text = text.replace(sentinel, sentinel + "\n" + content, 1)
|
|
38
|
+
path.write_text(text, encoding="utf-8")
|
|
39
|
+
return True
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Check PyPI package name availability using stdlib urllib (zero deps)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
PYPI_JSON_URL = "https://pypi.org/pypi/{name}/json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_name_available(name: str) -> dict:
|
|
14
|
+
"""Check if a package name is available on PyPI.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
dict with keys: name, available (bool), existing_version (str|None),
|
|
18
|
+
existing_description (str|None), suggestion (str|None)
|
|
19
|
+
"""
|
|
20
|
+
url = PYPI_JSON_URL.format(name=name)
|
|
21
|
+
try:
|
|
22
|
+
req = urllib.request.Request(url, method="GET")
|
|
23
|
+
req.add_header("Accept", "application/json")
|
|
24
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
25
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
26
|
+
info = data.get("info", {})
|
|
27
|
+
return {
|
|
28
|
+
"name": name,
|
|
29
|
+
"available": False,
|
|
30
|
+
"existing_version": info.get("version"),
|
|
31
|
+
"existing_description": info.get("summary"),
|
|
32
|
+
"suggestion": f'Try "{name}-mcp" or add a unique prefix.',
|
|
33
|
+
}
|
|
34
|
+
except urllib.error.HTTPError as e:
|
|
35
|
+
if e.code == 404:
|
|
36
|
+
return {
|
|
37
|
+
"name": name,
|
|
38
|
+
"available": True,
|
|
39
|
+
"existing_version": None,
|
|
40
|
+
"existing_description": None,
|
|
41
|
+
"suggestion": None,
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
"name": name,
|
|
45
|
+
"available": None,
|
|
46
|
+
"error": f"PyPI returned HTTP {e.code}",
|
|
47
|
+
"suggestion": "Try again in a moment.",
|
|
48
|
+
}
|
|
49
|
+
except (urllib.error.URLError, TimeoutError) as e:
|
|
50
|
+
return {
|
|
51
|
+
"name": name,
|
|
52
|
+
"available": None,
|
|
53
|
+
"error": str(e),
|
|
54
|
+
"suggestion": "Could not reach PyPI. Check your internet connection.",
|
|
55
|
+
}
|