@sakanano-moe/agenthelm 0.0.0-stage → 6.5.1

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.
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ """Launch one stdio MCP after loading a project-local dotenv file.
3
+
4
+ The launcher never prints dotenv values. Configuration contains only variable
5
+ references and is generated into a client-local project capability package.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import os
13
+ import re
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ REFERENCE = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
20
+
21
+
22
+ def load_dotenv(path: Path) -> dict[str, str]:
23
+ if path.is_symlink() or not path.is_file():
24
+ raise RuntimeError(f"credential env file missing or unsafe: {path}")
25
+ values: dict[str, str] = {}
26
+ for raw_line in path.read_text(encoding="utf-8").splitlines():
27
+ line = raw_line.strip()
28
+ if not line or line.startswith("#"):
29
+ continue
30
+ if line.startswith("export "):
31
+ line = line[7:].lstrip()
32
+ key, separator, value = line.partition("=")
33
+ key = key.strip()
34
+ if not separator or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key):
35
+ continue
36
+ value = value.strip()
37
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
38
+ value = value[1:-1]
39
+ values[key] = value
40
+ return values
41
+
42
+
43
+ def expand(value: Any, environment: dict[str, str]) -> Any:
44
+ if isinstance(value, str):
45
+ def replace(match: re.Match[str]) -> str:
46
+ name = match.group(1)
47
+ if name not in environment:
48
+ raise RuntimeError(f"credential env file missing key: {name}")
49
+ return environment[name]
50
+
51
+ return REFERENCE.sub(replace, value)
52
+ if isinstance(value, list):
53
+ return [expand(item, environment) for item in value]
54
+ if isinstance(value, dict):
55
+ return {key: expand(item, environment) for key, item in value.items()}
56
+ return value
57
+
58
+
59
+ def main(argv: list[str] | None = None) -> int:
60
+ parser = argparse.ArgumentParser()
61
+ parser.add_argument("--env-file", type=Path, required=True)
62
+ parser.add_argument("--config", type=Path, required=True)
63
+ args = parser.parse_args(argv)
64
+ try:
65
+ config = json.loads(args.config.read_text(encoding="utf-8"))
66
+ if not isinstance(config, dict) or config.get("type") != "stdio":
67
+ raise RuntimeError("launcher config must be a stdio MCP object")
68
+ environment = dict(os.environ)
69
+ environment.update(load_dotenv(args.env_file))
70
+ config = expand(config, environment)
71
+ command = config.get("command")
72
+ command_args = config.get("args", [])
73
+ if not isinstance(command, str) or not command:
74
+ raise RuntimeError("stdio command is required")
75
+ if not isinstance(command_args, list) or not all(isinstance(item, str) for item in command_args):
76
+ raise RuntimeError("stdio args must be a string array")
77
+ child_env = dict(environment)
78
+ raw_env = config.get("env", {})
79
+ if not isinstance(raw_env, dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in raw_env.items()):
80
+ raise RuntimeError("stdio env must be a string object")
81
+ child_env.update(raw_env)
82
+ cwd = config.get("cwd")
83
+ if cwd is not None:
84
+ if not isinstance(cwd, str):
85
+ raise RuntimeError("stdio cwd must be a string")
86
+ os.chdir(cwd)
87
+ os.execvpe(command, [command, *command_args], child_env)
88
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError, RuntimeError) as exc:
89
+ print(f"agent-pack MCP launch failed: {exc}", file=sys.stderr)
90
+ return 2
91
+
92
+
93
+ if __name__ == "__main__":
94
+ raise SystemExit(main())
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env python3
2
+ """MCP stdio adapter for agent-pack capability-management operations.
3
+
4
+ Primary protocol: MCP 2026-07-28 (stateless, per-request metadata).
5
+ Compatibility: legacy initialize/notifications/initialized clients.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from agent_pack_config import (
16
+ AgentPackError,
17
+ doctor,
18
+ install_plugin,
19
+ inventory_source,
20
+ reconcile_project,
21
+ sync_project,
22
+ transfer_to_project,
23
+ )
24
+
25
+
26
+ PROTOCOL_VERSION = "2026-07-28"
27
+ LEGACY_VERSION = "2025-06-18"
28
+ def _plugin_version() -> str:
29
+ """Single source of truth: the sibling plugin.json version."""
30
+ try:
31
+ manifest = json.loads((Path(__file__).resolve().parents[1] / "plugin.json").read_text(encoding="utf-8"))
32
+ version = manifest.get("version")
33
+ return version if isinstance(version, str) else "0.0.0"
34
+ except (OSError, json.JSONDecodeError):
35
+ return "0.0.0"
36
+
37
+
38
+ SERVER_INFO = {"name": "agent-pack", "version": _plugin_version()}
39
+ SERVER_META = {"io.modelcontextprotocol/serverInfo": SERVER_INFO}
40
+
41
+
42
+ TOOLS = [
43
+ {
44
+ "name": "agent_pack_bootstrap",
45
+ "title": "Install Agent Plugin",
46
+ "description": "Dry-run or install a portable plugin, including agent-pack itself, into Codex, Cursor, or Claude Code without overwriting an existing package.",
47
+ "inputSchema": {
48
+ "type": "object",
49
+ "properties": {
50
+ "plugin": {"type": "string"},
51
+ "client": {"type": "string", "enum": ["codex", "cursor", "claude"]},
52
+ "home": {"type": "string"},
53
+ "apply": {"type": "boolean", "default": False},
54
+ "replace": {"type": "boolean", "default": False},
55
+ "recoveryRoot": {"type": "string"},
56
+ },
57
+ "required": ["plugin", "client"],
58
+ "additionalProperties": False,
59
+ },
60
+ "annotations": {"destructiveHint": False, "idempotentHint": False},
61
+ },
62
+ {
63
+ "name": "agent_pack_reconcile",
64
+ "title": "Reconcile Project and Client Capabilities",
65
+ "description": "Classify capability drift, safely replace the managed project package, and archive explicitly selected legacy Skills or plugins with rollback. apply defaults to false.",
66
+ "inputSchema": {
67
+ "type": "object",
68
+ "properties": {
69
+ "project": {"type": "string"},
70
+ "client": {"type": "string", "enum": ["codex", "cursor"]},
71
+ "home": {"type": "string"},
72
+ "retireSkills": {"type": "array", "items": {"type": "string"}, "default": []},
73
+ "retirePlugins": {"type": "array", "items": {"type": "string"}, "default": []},
74
+ "recoveryRoot": {"type": "string"},
75
+ "retireOnly": {"type": "boolean", "default": False},
76
+ "apply": {"type": "boolean", "default": False},
77
+ },
78
+ "required": ["project", "client"],
79
+ "additionalProperties": False,
80
+ },
81
+ "annotations": {"destructiveHint": True, "idempotentHint": False},
82
+ },
83
+ {
84
+ "name": "agent_pack_inventory",
85
+ "title": "Inventory Agent Capabilities",
86
+ "description": "Discover portable Agent Plugins, Skills, and MCP servers from a project, plugin directory, or supported client.",
87
+ "inputSchema": {
88
+ "type": "object",
89
+ "properties": {
90
+ "source": {"type": "string", "description": "project:/path, plugin:/path, client:codex, client:cursor, or client:claude"},
91
+ "home": {"type": "string"},
92
+ },
93
+ "required": ["source"],
94
+ "additionalProperties": False,
95
+ },
96
+ },
97
+ {
98
+ "name": "agent_pack_doctor",
99
+ "title": "Validate Project Capabilities",
100
+ "description": "Validate an initialized project's Agent Plugin, Skill, MCP, duplicate-name, portability, and secret boundaries.",
101
+ "inputSchema": {
102
+ "type": "object",
103
+ "properties": {"project": {"type": "string"}},
104
+ "required": ["project"],
105
+ "additionalProperties": False,
106
+ },
107
+ },
108
+ {
109
+ "name": "agent_pack_transfer",
110
+ "title": "Transfer Skills and MCP Servers",
111
+ "description": "Dry-run or copy explicitly selected portable Skills and MCP definitions into another initialized project. apply defaults to false.",
112
+ "inputSchema": {
113
+ "type": "object",
114
+ "properties": {
115
+ "source": {"type": "string"},
116
+ "toProject": {"type": "string"},
117
+ "skills": {"type": "array", "items": {"type": "string"}, "default": []},
118
+ "mcps": {"type": "array", "items": {"type": "string"}, "default": []},
119
+ "home": {"type": "string"},
120
+ "apply": {"type": "boolean", "default": False},
121
+ },
122
+ "required": ["source", "toProject"],
123
+ "additionalProperties": False,
124
+ },
125
+ "annotations": {"destructiveHint": False, "idempotentHint": False},
126
+ },
127
+ {
128
+ "name": "agent_pack_sync",
129
+ "title": "Sync Project Capabilities to an Agent Client",
130
+ "description": "Dry-run or install a project's portable capability package into Codex or Cursor. apply defaults to false and existing targets are never overwritten.",
131
+ "inputSchema": {
132
+ "type": "object",
133
+ "properties": {
134
+ "project": {"type": "string"},
135
+ "client": {"type": "string", "enum": ["codex", "cursor"]},
136
+ "home": {"type": "string"},
137
+ "apply": {"type": "boolean", "default": False},
138
+ },
139
+ "required": ["project", "client"],
140
+ "additionalProperties": False,
141
+ },
142
+ "annotations": {"destructiveHint": False, "idempotentHint": False},
143
+ },
144
+ ]
145
+
146
+
147
+ def _modern(params: dict[str, Any]) -> bool:
148
+ meta = params.get("_meta", {})
149
+ return isinstance(meta, dict) and meta.get("io.modelcontextprotocol/protocolVersion") == PROTOCOL_VERSION
150
+
151
+
152
+ def _result(payload: dict[str, Any], modern: bool) -> dict[str, Any]:
153
+ result: dict[str, Any] = {
154
+ "content": [{"type": "text", "text": json.dumps(payload, ensure_ascii=False, indent=2)}],
155
+ "structuredContent": payload,
156
+ "isError": not payload.get("ok", True),
157
+ }
158
+ if modern:
159
+ result["resultType"] = "complete"
160
+ result["_meta"] = SERVER_META
161
+ return result
162
+
163
+
164
+ def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
165
+ home_value = arguments.get("home")
166
+ home = Path(home_value) if isinstance(home_value, str) else None
167
+ if name == "agent_pack_bootstrap":
168
+ recovery_value = arguments.get("recoveryRoot")
169
+ recovery = Path(recovery_value) if isinstance(recovery_value, str) else None
170
+ return install_plugin(
171
+ Path(arguments["plugin"]),
172
+ arguments["client"],
173
+ home=home,
174
+ apply=bool(arguments.get("apply", False)),
175
+ replace=bool(arguments.get("replace", False)),
176
+ recovery_root=recovery,
177
+ )
178
+ if name == "agent_pack_reconcile":
179
+ recovery_value = arguments.get("recoveryRoot")
180
+ recovery = Path(recovery_value) if isinstance(recovery_value, str) else None
181
+ return reconcile_project(
182
+ Path(arguments["project"]),
183
+ arguments["client"],
184
+ home=home,
185
+ retire_skills=arguments.get("retireSkills", []),
186
+ retire_plugins=arguments.get("retirePlugins", []),
187
+ recovery_root=recovery,
188
+ apply=bool(arguments.get("apply", False)),
189
+ retire_only=bool(arguments.get("retireOnly", False)),
190
+ )
191
+ if name == "agent_pack_inventory":
192
+ inventory = inventory_source(arguments["source"], home)
193
+ payload = inventory.to_dict()
194
+ payload["ok"] = not payload["issues"]
195
+ return payload
196
+ if name == "agent_pack_doctor":
197
+ return doctor(Path(arguments["project"]))
198
+ if name == "agent_pack_transfer":
199
+ source = inventory_source(arguments["source"], home)
200
+ return transfer_to_project(
201
+ source,
202
+ Path(arguments["toProject"]),
203
+ arguments.get("skills", []),
204
+ arguments.get("mcps", []),
205
+ apply=bool(arguments.get("apply", False)),
206
+ )
207
+ if name == "agent_pack_sync":
208
+ return sync_project(
209
+ Path(arguments["project"]),
210
+ arguments["client"],
211
+ home=home,
212
+ apply=bool(arguments.get("apply", False)),
213
+ )
214
+ raise AgentPackError(f"unknown tool: {name}")
215
+
216
+
217
+ def handle_message(message: dict[str, Any]) -> dict[str, Any] | None:
218
+ if message.get("jsonrpc") != "2.0":
219
+ return {"jsonrpc": "2.0", "id": message.get("id"), "error": {"code": -32600, "message": "Invalid Request"}}
220
+ if "id" not in message:
221
+ return None
222
+ request_id = message["id"]
223
+ method = message.get("method")
224
+ params = message.get("params") if isinstance(message.get("params"), dict) else {}
225
+ modern = _modern(params)
226
+ try:
227
+ if method == "server/discover":
228
+ result = {
229
+ "resultType": "complete",
230
+ "supportedVersions": [PROTOCOL_VERSION],
231
+ "capabilities": {"tools": {}},
232
+ "_meta": SERVER_META,
233
+ "instructions": "Inventory first. Transfer and sync default to dry-run; set apply=true only after user approval.",
234
+ "ttlMs": 3600000,
235
+ "cacheScope": "public",
236
+ }
237
+ elif method == "initialize":
238
+ result = {
239
+ "protocolVersion": LEGACY_VERSION,
240
+ "capabilities": {"tools": {}},
241
+ "serverInfo": SERVER_INFO,
242
+ "instructions": "Inventory first; write operations require apply=true.",
243
+ }
244
+ elif method == "tools/list":
245
+ result = {"tools": TOOLS}
246
+ if modern:
247
+ result.update({
248
+ "resultType": "complete",
249
+ "ttlMs": 300000,
250
+ "cacheScope": "public",
251
+ "_meta": SERVER_META,
252
+ })
253
+ elif method == "tools/call":
254
+ name = params.get("name")
255
+ arguments = params.get("arguments", {})
256
+ if not isinstance(name, str) or not isinstance(arguments, dict):
257
+ raise AgentPackError("tools/call requires string name and object arguments")
258
+ result = _result(_call(name, arguments), modern)
259
+ else:
260
+ return {"jsonrpc": "2.0", "id": request_id, "error": {"code": -32601, "message": "Method not found"}}
261
+ return {"jsonrpc": "2.0", "id": request_id, "result": result}
262
+ except (AgentPackError, KeyError, TypeError, ValueError) as exc:
263
+ payload = {"ok": False, "error": str(exc)}
264
+ if method == "tools/call":
265
+ return {"jsonrpc": "2.0", "id": request_id, "result": _result(payload, modern)}
266
+ return {"jsonrpc": "2.0", "id": request_id, "error": {"code": -32602, "message": str(exc)}}
267
+
268
+
269
+ def main() -> int:
270
+ for line in sys.stdin:
271
+ try:
272
+ message = json.loads(line)
273
+ if not isinstance(message, dict):
274
+ raise ValueError("message must be an object")
275
+ response = handle_message(message)
276
+ except (json.JSONDecodeError, ValueError) as exc:
277
+ response = {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": str(exc)}}
278
+ if response is not None:
279
+ sys.stdout.write(json.dumps(response, ensure_ascii=False, separators=(",", ":")) + "\n")
280
+ sys.stdout.flush()
281
+ return 0
282
+
283
+
284
+ if __name__ == "__main__":
285
+ raise SystemExit(main())
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.