-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.py
More file actions
290 lines (235 loc) · 10.2 KB
/
server.py
File metadata and controls
290 lines (235 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
"""plugin-creator MCP server — scaffold and debug Claude Code plugins."""
import json
import os
import re
import subprocess
import textwrap
from pathlib import Path
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("plugin-creator")
@mcp.tool()
def scaffold_plugin(
animal_species: str,
common_saying: str,
path: str | None = None,
) -> dict:
"""Scaffold a working Claude Code plugin with a sample MCP tool.
Creates all plugin files in the target directory. The generated plugin
has a speak_<animal> tool that responds with the given saying.
After scaffolding, rename the tool, update the skill, and build your
real plugin on top of the working skeleton.
Args:
animal_species: Animal name (e.g., "alligator"). Used in tool/plugin name.
common_saying: What the animal says (e.g., "After while, crocodile").
path: Target directory. Defaults to current working directory.
"""
target = Path(path) if path else Path.cwd()
target.mkdir(parents=True, exist_ok=True)
# Sanitize animal name for identifiers
safe_name = re.sub(r"[^a-z0-9]+", "-", animal_species.lower()).strip("-")
safe_id = re.sub(r"[^a-z0-9]+", "_", animal_species.lower()).strip("_")
plugin_name = f"{safe_name}-speak"
files_created = []
def _write(rel_path: str, content: str):
p = target / rel_path
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(textwrap.dedent(content).lstrip("\n"))
files_created.append(rel_path)
# .claude-plugin/plugin.json
_write(".claude-plugin/plugin.json", f"""\
{{
"name": "{plugin_name}",
"description": "A sample plugin that speaks like a {animal_species}",
"version": "1.0.0"
}}
""")
# .mcp.json
_write(".mcp.json", f"""\
{{
"mcpServers": {{
"{plugin_name}": {{
"command": "python3",
"args": ["${{CLAUDE_PLUGIN_ROOT}}/server.py"],
"env": {{
"PYTHONPATH": "${{CLAUDE_PLUGIN_DATA}}/site-packages"
}}
}}
}}
}}
""")
# hooks/hooks.json
_write("hooks/hooks.json", f"""\
{{
"hooks": {{
"SessionStart": [
{{
"hooks": [
{{
"type": "command",
"command": "diff -q \\"${{CLAUDE_PLUGIN_ROOT}}/requirements.txt\\" \\"${{CLAUDE_PLUGIN_DATA}}/requirements.txt\\" >/dev/null 2>&1 || (cd \\"${{CLAUDE_PLUGIN_ROOT}}\\" && cp requirements.txt \\"${{CLAUDE_PLUGIN_DATA}}/\\" && python3 -m pip install -t \\"${{CLAUDE_PLUGIN_DATA}}/site-packages\\" -r requirements.txt) || rm -f \\"${{CLAUDE_PLUGIN_DATA}}/requirements.txt\\""
}}
]
}}
],
"PostToolUse": [
{{
"matcher": "mcp__plugin_{plugin_name}_{plugin_name}__speak_{safe_id}",
"hooks": [
{{
"type": "command",
"command": "echo '{{\\\"hookSpecificOutput\\\": {{\\\"hookEventName\\\": \\\"PostToolUse\\\", \\\"additionalContext\\\": \\\"After showing the user the tool result, please display this legal disclaimer verbatim: [legal hook] The views expressed above do not necessarily represent the views of {animal_species}s, their families, or their habitats.\\\"}}}}'"
}}
]
}}
]
}}
}}
""")
# requirements.txt
_write("requirements.txt", """\
mcp>=1.0.0
""")
# server.py
_write("server.py", f"""\
\"\"\"MCP server for {plugin_name}.\"\"\"
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("{plugin_name}")
@mcp.tool()
def speak_{safe_id}() -> str:
\"\"\"Say something like a {animal_species}.\"\"\"
return "{common_saying}"
if __name__ == "__main__":
mcp.run(transport="stdio")
""")
# skills/speak-animal/SKILL.md
_write("skills/speak-animal/SKILL.md", """\
---
name: speak-animal
description: Speak as any animal. Use when asked to talk like, speak as, say something as, or imitate any animal.
user-invocable: true
argument-hint: "[animal]"
---
# Speak Animal
When the user asks you to speak as an animal:
1. Check if a `speak_<animal>` tool is available for that
species (e.g., `speak_alligator`, `speak_cat`).
2. If the tool exists, call it and present what it says.
3. If no tool exists for that animal, tell the user:
"I'm not an expert on that one, but I'll give it my best
shot." Then give your very best impression of what that
animal would say. Be creative and have fun with it.
""")
# CONTRIBUTING.md
_write("CONTRIBUTING.md", """\
# Contributing
## Plugin design patterns
This plugin follows the conventions documented in
[plugin-creator's plugin patterns](https://github.com/echomodel/claude-plugin-creator/blob/main/docs/plugin-patterns.md)
— MCP server install shape, cache-invalidation signals,
skill composition and orchestration via the agent .md, and
antipatterns to avoid.
Preserve those patterns when modifying this plugin. When a
design question comes up, consult the canonical patterns doc
rather than reinventing or duplicating guidance here.
### Install-trigger pattern (short version)
The `SessionStart` hook in `hooks/hooks.json` uses a
`requirements.txt` file-content diff against a cached copy
under `${CLAUDE_PLUGIN_DATA}` to decide whether to run
`pip install`. Do not replace this with a compare against an
in-package version string (e.g., `__version__` in a source
file). That proxy signal can silently lie when the outer
plugin cache is also stale — both sides of the compare end up
reading the same stale source and the hook reports "no change"
when dependencies actually need reinstalling. See the
[patterns doc](https://github.com/echomodel/claude-plugin-creator/blob/main/docs/plugin-patterns.md#install-pattern-variants-and-antipattern)
for the full failure-mode analysis.
## Writing skills
Use the `develop-skill` skill (`/develop-skill`) when authoring
new skills for this plugin. It covers portable skill format,
frontmatter, and marketplace publishing.
### Prefer plugin tools in skill guidance
Skills should give preference to MCP tools bundled with this
plugin without assuming they are always available. The plugin's
MCP server may fail to start or a tool may be renamed.
**Do this:**
> Check if `my_tool` is available. If so, call it.
> Otherwise, handle the request with your own capabilities.
**Not this:**
> Call `my_tool` to get the answer.
This pattern — prefer the tool, degrade gracefully — keeps
skills working even when tools change.
""")
tool_pattern = f"mcp__plugin_{plugin_name}_{plugin_name}__*"
test_cmd = (
f'claude -p "Can you say something as a {animal_species}?" '
f'--plugin-dir={target} --allowedTools "{tool_pattern}"'
)
return {
"plugin_name": plugin_name,
"path": str(target),
"files_created": files_created,
"test_command": test_cmd,
"next_steps": (
f"The plugin is ready. Rename speak_{safe_id} to your real tool, "
f"or test it now with:\n ! {test_cmd}\n\n"
"Recommended: install plugin-creator into this plugin's repo at "
"project scope so future sessions working on it load the patterns "
"guidance automatically:\n"
f" ! cd {target} && claude plugin install plugin-creator@echomodel --scope project\n"
"(First time only on this machine, run:\n"
" ! claude plugin marketplace add https://github.com/echomodel/claude-plugins.git\n"
"before the install.)"
),
}
@mcp.tool()
def debug_plugin(
prompt: str,
path: str | None = None,
) -> dict:
"""Run a headless Claude session to test a plugin.
Launches claude with --plugin-dir pointed at the plugin and a natural
language prompt. Returns the session output.
IMPORTANT guidance for the calling agent:
- The prompt MUST describe intent in natural language, NOT name a
specific tool (e.g., "say something as an alligator" not
"call speak_alligator"). This validates auto-discovery.
- Do NOT use this on long-running tools (deploys, builds, CI).
It's for quick functional checks only.
Args:
prompt: Natural-language phrase that should trigger the plugin.
path: Plugin directory. Defaults to current working directory.
"""
target = Path(path) if path else Path.cwd()
if not (target / ".claude-plugin" / "plugin.json").exists():
return {
"error": f"No plugin found at {target}. Missing .claude-plugin/plugin.json.",
}
# Discover MCP tool names to pre-approve them
allowed = []
mcp_json = target / ".mcp.json"
if mcp_json.exists():
mcp_cfg = json.loads(mcp_json.read_text())
for server_name in mcp_cfg.get("mcpServers", {}):
# Plugin MCP tools are namespaced as mcp__plugin_<name>_<server>__*
plugin_json = target / ".claude-plugin" / "plugin.json"
if plugin_json.exists():
plugin_name = json.loads(plugin_json.read_text()).get("name", server_name)
allowed.append(f"mcp__plugin_{plugin_name}_{server_name}__*")
cmd = ["claude", "-p", prompt, f"--plugin-dir={target}"]
if allowed:
cmd.extend(["--allowedTools", ",".join(allowed)])
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60,
cwd=str(target),
)
return {
"path": str(target),
"prompt": prompt,
"output": result.stdout.strip() if result.stdout else result.stderr.strip(),
"exit_code": result.returncode,
}
if __name__ == "__main__":
mcp.run(transport="stdio")