@event4u/agent-config 1.36.1 → 1.38.0
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +77 -0
- package/README.md +25 -1
- package/docs/contracts/mcp-cloud-scope.md +182 -0
- package/docs/contracts/mcp-phase-1-scope.md +195 -0
- package/docs/guidelines/agent-infra/mcp-request-signing.md +4 -0
- package/docs/mcp-server.md +164 -0
- package/docs/setup/mcp-cloud-endpoints.md +93 -0
- package/docs/setup/mcp-cloud-registry-listing.md +99 -0
- package/docs/setup/mcp-cloud-setup.md +152 -0
- package/docs/setup/mcp-r2-bootstrap.md +82 -0
- package/docs/setup/mcp-server-docker.md +97 -0
- package/package.json +1 -1
- package/scripts/agent-config +29 -0
- package/scripts/mcp_parity_smoke.py +146 -0
- package/scripts/mcp_server/__init__.py +13 -0
- package/scripts/mcp_server/__main__.py +12 -0
- package/scripts/mcp_server/metadata.py +75 -0
- package/scripts/mcp_server/prompts.py +305 -0
- package/scripts/mcp_server/requirements.txt +4 -0
- package/scripts/mcp_server/resources.py +201 -0
- package/scripts/mcp_server/server.py +269 -0
- package/scripts/mcp_server/tools.py +363 -0
- package/scripts/mcp_setup.sh +87 -0
- package/scripts/pack_mcp_content.py +274 -0
- package/scripts/readme_linter.py +1 -1
- package/scripts/skill_linter.py +7 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""MCP Server — Phase 4 tools layer.
|
|
2
|
+
|
|
3
|
+
A0 contract amendment: Phase 4 lifts the read-only line for exactly the
|
|
4
|
+
tools listed in ``ALLOWLIST`` (`lint_skills` + `chat_history_append`).
|
|
5
|
+
Every other tool name is unreachable — `tools/call` against an unknown
|
|
6
|
+
name raises ``ValueError``, not just "unlisted".
|
|
7
|
+
|
|
8
|
+
Path-scoping is mandatory for any tool that writes: the resolved target
|
|
9
|
+
path must stay under ``<consumer_root>`` and within the allowlist of
|
|
10
|
+
filenames (`.agent-chat-history`, `agents/.agent-chat-history`). Escape
|
|
11
|
+
attempts surface as ``ValueError`` before the underlying writer runs.
|
|
12
|
+
|
|
13
|
+
This module deliberately does **not** import the ``subprocess`` module
|
|
14
|
+
or ``os``-level shell-execution helpers directly. It imports project
|
|
15
|
+
modules (``skill_linter``, ``chat_history``) that internally use them;
|
|
16
|
+
the wire surface exposes no shell execution.
|
|
17
|
+
|
|
18
|
+
Tools return ``dict`` from their handlers — the SDK wraps that in a
|
|
19
|
+
``TextContent`` block with the JSON-serialized payload, so MCP clients
|
|
20
|
+
can render structured output.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Awaitable, Callable
|
|
28
|
+
|
|
29
|
+
# Allowlisted directories (relative to consumer_root) where tool writes
|
|
30
|
+
# are permitted. ``chat_history_append`` resolves its path through this
|
|
31
|
+
# guard before the underlying writer touches the filesystem.
|
|
32
|
+
_ALLOWED_WRITE_REL_PATHS: frozenset[str] = frozenset(
|
|
33
|
+
{
|
|
34
|
+
"agents/.agent-chat-history",
|
|
35
|
+
".agent-chat-history",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
ToolHandler = Callable[[dict[str, Any], Path], Awaitable[dict[str, Any]]]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class BuiltinTool:
|
|
45
|
+
"""Static registration record for an allowlisted MCP tool.
|
|
46
|
+
|
|
47
|
+
``input_schema`` is a JSON-Schema dict the SDK validates against on
|
|
48
|
+
each ``tools/call``. ``handler`` is an async function that receives
|
|
49
|
+
the validated arguments + the resolved ``consumer_root`` Path.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
name: str
|
|
53
|
+
description: str
|
|
54
|
+
input_schema: dict[str, Any]
|
|
55
|
+
handler: ToolHandler
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_consumer_root(override: Path | None = None) -> Path:
|
|
59
|
+
"""Pick the consumer-project root.
|
|
60
|
+
|
|
61
|
+
Default: the current working directory. Tests pass an explicit
|
|
62
|
+
override; the stdio entrypoint relies on the CWD set by the
|
|
63
|
+
``./agent-config mcp:run`` wrapper.
|
|
64
|
+
"""
|
|
65
|
+
if override is not None:
|
|
66
|
+
return override.resolve()
|
|
67
|
+
return Path.cwd().resolve()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _validate_in_tree_path(raw: str | None, consumer_root: Path) -> Path:
|
|
71
|
+
"""Resolve ``raw`` under ``consumer_root`` and assert it stays in tree.
|
|
72
|
+
|
|
73
|
+
Returns the resolved target path. Raises ``ValueError`` when the
|
|
74
|
+
path escapes the root or is not in the write allowlist. ``None``
|
|
75
|
+
falls back to the default chat-history location.
|
|
76
|
+
"""
|
|
77
|
+
root = consumer_root.resolve()
|
|
78
|
+
if raw is None or raw == "":
|
|
79
|
+
target = root / "agents" / ".agent-chat-history"
|
|
80
|
+
else:
|
|
81
|
+
candidate = Path(raw)
|
|
82
|
+
if candidate.is_absolute():
|
|
83
|
+
target = candidate.resolve()
|
|
84
|
+
else:
|
|
85
|
+
target = (root / candidate).resolve()
|
|
86
|
+
try:
|
|
87
|
+
rel = target.relative_to(root)
|
|
88
|
+
except ValueError as exc:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"path escapes consumer_root: {target} not under {root}"
|
|
91
|
+
) from exc
|
|
92
|
+
rel_str = rel.as_posix()
|
|
93
|
+
if rel_str not in _ALLOWED_WRITE_REL_PATHS:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"path not in write allowlist: {rel_str!r} "
|
|
96
|
+
f"(allowed: {sorted(_ALLOWED_WRITE_REL_PATHS)})"
|
|
97
|
+
)
|
|
98
|
+
return target
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def _lint_skills_handler(
|
|
102
|
+
arguments: dict[str, Any],
|
|
103
|
+
consumer_root: Path,
|
|
104
|
+
) -> dict[str, Any]:
|
|
105
|
+
"""D2 — read-only wrapper around ``skill_linter.lint_file``.
|
|
106
|
+
|
|
107
|
+
Arguments:
|
|
108
|
+
paths: optional list of repo-relative paths to lint. Empty /
|
|
109
|
+
missing → lint the full ``.agent-src.uncompressed/`` tree
|
|
110
|
+
via ``gather_all_candidate_files``.
|
|
111
|
+
|
|
112
|
+
Never spawns ``git`` (no ``--changed`` mode); never writes; mirrors
|
|
113
|
+
the JSON output format of ``scripts/skill_linter.py --format json``.
|
|
114
|
+
"""
|
|
115
|
+
# Import lazily so the loader-layer import-surface test stays clean.
|
|
116
|
+
from scripts.skill_linter import ( # noqa: PLC0415
|
|
117
|
+
format_json,
|
|
118
|
+
gather_all_candidate_files,
|
|
119
|
+
lint_file,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
root = consumer_root.resolve()
|
|
123
|
+
requested = arguments.get("paths") or []
|
|
124
|
+
if not isinstance(requested, list):
|
|
125
|
+
raise ValueError("'paths' must be a list of strings")
|
|
126
|
+
|
|
127
|
+
paths: list[Path] = []
|
|
128
|
+
if requested:
|
|
129
|
+
for raw in requested:
|
|
130
|
+
if not isinstance(raw, str):
|
|
131
|
+
raise ValueError("'paths' entries must be strings")
|
|
132
|
+
candidate = Path(raw)
|
|
133
|
+
resolved = (
|
|
134
|
+
candidate.resolve()
|
|
135
|
+
if candidate.is_absolute()
|
|
136
|
+
else (root / candidate).resolve()
|
|
137
|
+
)
|
|
138
|
+
try:
|
|
139
|
+
resolved.relative_to(root)
|
|
140
|
+
except ValueError as exc:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"path escapes consumer_root: {resolved}"
|
|
143
|
+
) from exc
|
|
144
|
+
if resolved.exists():
|
|
145
|
+
paths.append(resolved)
|
|
146
|
+
else:
|
|
147
|
+
paths = gather_all_candidate_files(root)
|
|
148
|
+
|
|
149
|
+
results = [lint_file(p, repo_root=root) for p in sorted(set(paths))]
|
|
150
|
+
payload = json.loads(format_json(results))
|
|
151
|
+
return payload
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def _chat_history_append_handler(
|
|
155
|
+
arguments: dict[str, Any],
|
|
156
|
+
consumer_root: Path,
|
|
157
|
+
) -> dict[str, Any]:
|
|
158
|
+
"""D3 — append one entry to the consumer's chat-history JSONL.
|
|
159
|
+
|
|
160
|
+
Arguments:
|
|
161
|
+
text: free-form entry text. Stored under the ``text`` field.
|
|
162
|
+
entry_type: short ``t`` tag (e.g. ``note``, ``decision``).
|
|
163
|
+
path: optional override of the target file. Must resolve to one
|
|
164
|
+
of ``agents/.agent-chat-history`` / ``.agent-chat-history``
|
|
165
|
+
under ``consumer_root``.
|
|
166
|
+
session: optional 16-char session tag. Falls back to the most
|
|
167
|
+
recent body entry's ``s`` (see ``chat_history.append``).
|
|
168
|
+
dry_run: when true, validates the payload + path guard and
|
|
169
|
+
returns the entry that *would* be written without touching
|
|
170
|
+
the filesystem.
|
|
171
|
+
min_schema_version: when set, the call fails fast if the
|
|
172
|
+
chat-history schema version is below this number. Defaults
|
|
173
|
+
to ``None`` (no version check).
|
|
174
|
+
|
|
175
|
+
# TODO(phase-6): wrap the file write in ``fcntl.flock`` for SSE /
|
|
176
|
+
# multi-process safety. stdio is single-process so this is a moot
|
|
177
|
+
# concern in Phase 4.
|
|
178
|
+
"""
|
|
179
|
+
from scripts.chat_history import ( # noqa: PLC0415
|
|
180
|
+
SCHEMA_VERSION,
|
|
181
|
+
append,
|
|
182
|
+
init,
|
|
183
|
+
read_header,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
text = arguments.get("text")
|
|
187
|
+
if not isinstance(text, str) or not text.strip():
|
|
188
|
+
raise ValueError("'text' must be a non-empty string")
|
|
189
|
+
entry_type = arguments.get("entry_type") or "note"
|
|
190
|
+
if not isinstance(entry_type, str) or not entry_type.strip():
|
|
191
|
+
raise ValueError("'entry_type' must be a non-empty string")
|
|
192
|
+
if entry_type == "header":
|
|
193
|
+
raise ValueError("'entry_type' must not be 'header'")
|
|
194
|
+
|
|
195
|
+
session = arguments.get("session")
|
|
196
|
+
if session is not None and not isinstance(session, str):
|
|
197
|
+
raise ValueError("'session' must be a string when provided")
|
|
198
|
+
|
|
199
|
+
dry_run = bool(arguments.get("dry_run", False))
|
|
200
|
+
|
|
201
|
+
raw_path = arguments.get("path")
|
|
202
|
+
target = _validate_in_tree_path(raw_path, consumer_root)
|
|
203
|
+
|
|
204
|
+
min_schema = arguments.get("min_schema_version")
|
|
205
|
+
if min_schema is not None:
|
|
206
|
+
if not isinstance(min_schema, int):
|
|
207
|
+
raise ValueError("'min_schema_version' must be an integer")
|
|
208
|
+
existing_header = read_header(target) if target.exists() else None
|
|
209
|
+
observed = (
|
|
210
|
+
int(existing_header.get("v", 0))
|
|
211
|
+
if isinstance(existing_header, dict)
|
|
212
|
+
else SCHEMA_VERSION
|
|
213
|
+
)
|
|
214
|
+
if observed < min_schema:
|
|
215
|
+
raise ValueError(
|
|
216
|
+
f"chat-history schema {observed} below required "
|
|
217
|
+
f"{min_schema}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
entry: dict[str, Any] = {"t": entry_type, "text": text}
|
|
221
|
+
|
|
222
|
+
if dry_run:
|
|
223
|
+
return {
|
|
224
|
+
"dry_run": True,
|
|
225
|
+
"target_path": str(target),
|
|
226
|
+
"entry": entry,
|
|
227
|
+
"session": session,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# `append` requires the parent directory and a header line. Lazy-init
|
|
231
|
+
# the JSONL when the consumer hasn't run `agent-config chat:init` yet.
|
|
232
|
+
if not target.exists() or read_header(target) is None:
|
|
233
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
init(path=target)
|
|
235
|
+
append(entry, path=target, session=session)
|
|
236
|
+
return {
|
|
237
|
+
"dry_run": False,
|
|
238
|
+
"target_path": str(target),
|
|
239
|
+
"entry": entry,
|
|
240
|
+
"session": session,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------
|
|
245
|
+
# Allowlist — hardcoded per AI Council Q1-a verdict (2026-05-10).
|
|
246
|
+
# Adding a tool here is a code-review event; settings cannot enable an
|
|
247
|
+
# unlisted tool. Boot-time stderr log enumerates the registered set.
|
|
248
|
+
# ---------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
ALLOWLIST: dict[str, BuiltinTool] = {
|
|
251
|
+
"lint_skills": BuiltinTool(
|
|
252
|
+
name="lint_skills",
|
|
253
|
+
description=(
|
|
254
|
+
"Lint skill / rule / command / guideline / persona markdown "
|
|
255
|
+
"files. Returns the same JSON payload as "
|
|
256
|
+
"`scripts/skill_linter.py --format json`. Read-only — never "
|
|
257
|
+
"writes or spawns git. Pass `paths` to lint a subset, omit "
|
|
258
|
+
"for a full tree scan."
|
|
259
|
+
),
|
|
260
|
+
input_schema={
|
|
261
|
+
"type": "object",
|
|
262
|
+
"properties": {
|
|
263
|
+
"paths": {
|
|
264
|
+
"type": "array",
|
|
265
|
+
"items": {"type": "string"},
|
|
266
|
+
"description": (
|
|
267
|
+
"Repo-relative paths to lint. Empty / missing → "
|
|
268
|
+
"full tree scan via gather_all_candidate_files."
|
|
269
|
+
),
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
"additionalProperties": False,
|
|
273
|
+
},
|
|
274
|
+
handler=_lint_skills_handler,
|
|
275
|
+
),
|
|
276
|
+
"chat_history_append": BuiltinTool(
|
|
277
|
+
name="chat_history_append",
|
|
278
|
+
description=(
|
|
279
|
+
"Append one entry to the consumer's chat-history JSONL "
|
|
280
|
+
"(`agents/.agent-chat-history`). Path-scoped — writes "
|
|
281
|
+
"outside the allowlist raise ValueError. Use `dry_run` to "
|
|
282
|
+
"preview the payload without touching the filesystem."
|
|
283
|
+
),
|
|
284
|
+
input_schema={
|
|
285
|
+
"type": "object",
|
|
286
|
+
"properties": {
|
|
287
|
+
"text": {"type": "string"},
|
|
288
|
+
"entry_type": {
|
|
289
|
+
"type": "string",
|
|
290
|
+
"description": (
|
|
291
|
+
"Short ``t`` tag (e.g. note, decision). "
|
|
292
|
+
"Defaults to ``note``."
|
|
293
|
+
),
|
|
294
|
+
},
|
|
295
|
+
"path": {
|
|
296
|
+
"type": "string",
|
|
297
|
+
"description": (
|
|
298
|
+
"Optional path override. Must resolve to "
|
|
299
|
+
"`agents/.agent-chat-history` or "
|
|
300
|
+
"`.agent-chat-history` under consumer_root."
|
|
301
|
+
),
|
|
302
|
+
},
|
|
303
|
+
"session": {"type": "string"},
|
|
304
|
+
"dry_run": {"type": "boolean", "default": False},
|
|
305
|
+
"min_schema_version": {"type": "integer"},
|
|
306
|
+
},
|
|
307
|
+
"required": ["text"],
|
|
308
|
+
"additionalProperties": False,
|
|
309
|
+
},
|
|
310
|
+
handler=_chat_history_append_handler,
|
|
311
|
+
),
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def to_mcp_tool_meta(tool: BuiltinTool) -> dict[str, Any]:
|
|
316
|
+
"""Render a ``BuiltinTool`` as kwargs for ``mcp.types.Tool``."""
|
|
317
|
+
return {
|
|
318
|
+
"name": tool.name,
|
|
319
|
+
"description": tool.description,
|
|
320
|
+
"inputSchema": tool.input_schema,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class ToolCache:
|
|
325
|
+
"""Hardcoded registry view of ``ALLOWLIST`` with a stable interface.
|
|
326
|
+
|
|
327
|
+
Kept as a class for symmetry with ``PromptCache`` / ``ResourceCache``.
|
|
328
|
+
No mtime check needed — the allowlist lives in source and changes
|
|
329
|
+
require a deploy.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
def __init__(self, registry: dict[str, BuiltinTool] | None = None) -> None:
|
|
333
|
+
self._registry: dict[str, BuiltinTool] = dict(
|
|
334
|
+
registry if registry is not None else ALLOWLIST
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def names(self) -> list[str]:
|
|
338
|
+
return sorted(self._registry.keys())
|
|
339
|
+
|
|
340
|
+
def list(self) -> list[BuiltinTool]:
|
|
341
|
+
return [self._registry[name] for name in self.names()]
|
|
342
|
+
|
|
343
|
+
def get(self, name: str) -> BuiltinTool | None:
|
|
344
|
+
return self._registry.get(name)
|
|
345
|
+
|
|
346
|
+
async def dispatch(
|
|
347
|
+
self,
|
|
348
|
+
name: str,
|
|
349
|
+
arguments: dict[str, Any],
|
|
350
|
+
consumer_root: Path | None = None,
|
|
351
|
+
) -> dict[str, Any]:
|
|
352
|
+
tool = self.get(name)
|
|
353
|
+
if tool is None:
|
|
354
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
355
|
+
root = _resolve_consumer_root(consumer_root)
|
|
356
|
+
return await tool.handler(arguments or {}, root)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def boot_log_line(cache: ToolCache) -> str:
|
|
360
|
+
"""Single-line stderr enumeration of the registered tools."""
|
|
361
|
+
names = cache.names()
|
|
362
|
+
return f"mcp-server: registered {len(names)} tools: {names}"
|
|
363
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# mcp_setup.sh — One-line MCP server onboarding.
|
|
3
|
+
# Creates .venv-mcp/ (gitignored) and installs the `mcp` SDK.
|
|
4
|
+
# Idempotent: safe to re-run; reuses an existing .venv-mcp/.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
VENV_DIR=".venv-mcp"
|
|
9
|
+
|
|
10
|
+
log_ok() { echo "✅ $*"; }
|
|
11
|
+
log_warn() { echo "⚠️ $*" >&2; }
|
|
12
|
+
log_err() { echo "❌ $*" >&2; }
|
|
13
|
+
|
|
14
|
+
# --- Locate a Python ≥ 3.11 ---
|
|
15
|
+
find_python() {
|
|
16
|
+
for cand in python3.13 python3.12 python3.11; do
|
|
17
|
+
if command -v "$cand" >/dev/null 2>&1; then
|
|
18
|
+
echo "$cand"
|
|
19
|
+
return 0
|
|
20
|
+
fi
|
|
21
|
+
done
|
|
22
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
23
|
+
local ver
|
|
24
|
+
ver="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')"
|
|
25
|
+
case "$ver" in
|
|
26
|
+
3.11|3.12|3.13|3.1[4-9]|3.[2-9][0-9]) echo "python3"; return 0 ;;
|
|
27
|
+
esac
|
|
28
|
+
fi
|
|
29
|
+
return 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
PY="$(find_python || true)"
|
|
33
|
+
if [[ -z "${PY:-}" ]]; then
|
|
34
|
+
log_err "Python 3.11+ not found."
|
|
35
|
+
log_err "Install Python 3.11+ (e.g. via pyenv, asdf, brew, or apt) and re-run."
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
PY_VER="$("$PY" -c 'import sys; print("%d.%d.%d" % sys.version_info[:3])')"
|
|
40
|
+
|
|
41
|
+
# --- Create or reuse venv ---
|
|
42
|
+
if [[ -d "$VENV_DIR" ]]; then
|
|
43
|
+
log_ok "$VENV_DIR/ exists — reusing (Python $("$VENV_DIR/bin/python" --version 2>&1 | awk '{print $2}'))"
|
|
44
|
+
else
|
|
45
|
+
"$PY" -m venv "$VENV_DIR"
|
|
46
|
+
log_ok "Created $VENV_DIR/ with $PY ($PY_VER)"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# --- Install / upgrade mcp SDK ---
|
|
50
|
+
"$VENV_DIR/bin/pip" install --quiet --upgrade pip
|
|
51
|
+
"$VENV_DIR/bin/pip" install --quiet --upgrade mcp
|
|
52
|
+
|
|
53
|
+
MCP_VER="$("$VENV_DIR/bin/python" -c 'import mcp, importlib.metadata as m; print(m.version("mcp"))' 2>/dev/null || echo "?")"
|
|
54
|
+
log_ok "Installed mcp SDK ($MCP_VER) in $VENV_DIR/"
|
|
55
|
+
|
|
56
|
+
# --- Smoke: import the server module ---
|
|
57
|
+
if ! "$VENV_DIR/bin/python" -c 'import scripts.mcp_server' 2>/dev/null; then
|
|
58
|
+
log_warn "scripts.mcp_server import failed — check repository checkout."
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
log_ok "scripts.mcp_server import OK"
|
|
62
|
+
|
|
63
|
+
# --- Print client config snippet ---
|
|
64
|
+
ROOT="$(pwd)"
|
|
65
|
+
PY_BIN="$ROOT/$VENV_DIR/bin/python"
|
|
66
|
+
|
|
67
|
+
echo ""
|
|
68
|
+
echo "── MCP server ready ─────────────────────────────────────────"
|
|
69
|
+
echo ""
|
|
70
|
+
echo "Run over stdio:"
|
|
71
|
+
echo " task mcp:run"
|
|
72
|
+
echo ""
|
|
73
|
+
echo "Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):"
|
|
74
|
+
cat <<JSON
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"agent-config": {
|
|
78
|
+
"command": "$PY_BIN",
|
|
79
|
+
"args": ["-m", "scripts.mcp_server"],
|
|
80
|
+
"env": { "PYTHONPATH": "$ROOT" }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
JSON
|
|
85
|
+
echo ""
|
|
86
|
+
echo "After saving the config: ⌘Q Claude Desktop and restart."
|
|
87
|
+
echo "──────────────────────────────────────────────────────────────"
|