@event4u/agent-config 1.39.0 → 1.41.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/.agent-src/commands/orchestrate.md +123 -0
- package/.agent-src/commands/sync-gitignore/fix.md +135 -0
- package/.agent-src/commands/sync-gitignore.md +31 -5
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +30 -2
- package/.agent-src/skills/subagent-orchestration/SKILL.md +9 -0
- package/.agent-src/skills/using-git-worktrees/SKILL.md +25 -0
- package/.agent-src/templates/agent-settings.md +9 -0
- package/.agent-src/templates/scripts/work_engine/orchestration.py +168 -0
- package/.claude-plugin/marketplace.json +3 -1
- package/CHANGELOG.md +75 -0
- package/README.md +52 -26
- package/bin/install.php +13 -6
- package/config/agent-settings.template.yml +21 -0
- package/docs/DISTRIBUTION_CHECKLIST.md +169 -0
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +5 -3
- package/docs/contracts/audit-log-v1.md +142 -0
- package/docs/contracts/command-clusters.md +2 -0
- package/docs/contracts/file-ownership-matrix.json +47 -0
- package/docs/contracts/mcp-discovery-phase-notice.md +56 -0
- package/docs/contracts/mcp-tool-stub-envelope.md +78 -0
- package/docs/contracts/orchestration-dsl-v1.md +152 -0
- package/docs/getting-started.md +1 -1
- package/docs/installation.md +132 -0
- package/docs/setup/mcp-client-config.md +94 -13
- package/docs/setup/mcp-cloud-setup.md +32 -1
- package/docs/setup/per-ide/aider.md +48 -0
- package/docs/setup/per-ide/claude-code.md +108 -0
- package/docs/setup/per-ide/claude-desktop.md +173 -0
- package/docs/setup/per-ide/cline.md +43 -0
- package/docs/setup/per-ide/codex.md +46 -0
- package/docs/setup/per-ide/copilot.md +80 -0
- package/docs/setup/per-ide/cursor.md +125 -0
- package/docs/setup/per-ide/gemini-cli.md +45 -0
- package/docs/setup/per-ide/windsurf.md +120 -0
- package/package.json +1 -1
- package/scripts/_lib/script_output.py +15 -11
- package/scripts/ai_council/session.py +14 -8
- package/scripts/chat_history.py +29 -53
- package/scripts/command_suggester/settings.py +15 -13
- package/scripts/compile_router.py +13 -9
- package/scripts/compress.py +175 -20
- package/scripts/council_cli.py +9 -3
- package/scripts/extract_audit_patterns.py +202 -0
- package/scripts/install +156 -1
- package/scripts/install.py +270 -10
- package/scripts/install.sh +52 -7
- package/scripts/lint_orchestration_dsl.py +214 -0
- package/scripts/mcp_parity_smoke.py +20 -2
- package/scripts/mcp_server/catalog.py +125 -0
- package/scripts/mcp_server/consumer_tool_catalog.json +275 -0
- package/scripts/mcp_server/telemetry.py +128 -0
- package/scripts/mcp_server/tools.py +474 -15
- package/scripts/mcp_telemetry_health.py +214 -0
- package/scripts/mcp_telemetry_query.py +203 -0
- package/scripts/mcp_telemetry_store.py +211 -0
- package/scripts/memory_signal.py +12 -10
- package/scripts/pack_mcp_content.py +18 -4
- package/scripts/skill_linter.py +9 -0
- package/scripts/sync_gitignore.py +56 -1
- package/templates/claude_desktop_config.json.template +22 -0
- package/templates/cursor-rule.mdc.j2 +7 -0
- package/templates/global-install-manifest.yml +91 -0
- package/templates/marketing-copy.yml +64 -0
- package/templates/windsurf-rule.md.j2 +7 -0
|
@@ -29,8 +29,10 @@ from typing import Any
|
|
|
29
29
|
_SCRIPTS = Path(__file__).resolve().parent
|
|
30
30
|
sys.path.insert(0, str(_SCRIPTS))
|
|
31
31
|
|
|
32
|
+
from mcp_server.catalog import load_catalog # noqa: E402
|
|
32
33
|
from mcp_server.prompts import load_all_prompts, to_mcp_prompt_meta # noqa: E402
|
|
33
34
|
from mcp_server.resources import load_all_resources, to_mcp_resource_meta # noqa: E402
|
|
35
|
+
from mcp_server.tools import ALLOWLIST # noqa: E402
|
|
34
36
|
|
|
35
37
|
PAGE_SIZE = 50
|
|
36
38
|
|
|
@@ -96,6 +98,21 @@ def _normalize_resources(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
96
98
|
return sorted(out, key=lambda x: x["uri"])
|
|
97
99
|
|
|
98
100
|
|
|
101
|
+
def _local_tools_list() -> dict[str, Any]:
|
|
102
|
+
"""Tools catalog + allowlist as the stdio server publishes them."""
|
|
103
|
+
catalog_names = [c.name for c in load_catalog()]
|
|
104
|
+
allowlist_names = list(ALLOWLIST.keys())
|
|
105
|
+
return {"tools": [{"name": n} for n in sorted(set(catalog_names + allowlist_names))]}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _normalize_tools(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|
109
|
+
"""Compare on `name` only — descriptions / schemas drift acceptably."""
|
|
110
|
+
return sorted(
|
|
111
|
+
[{"name": t["name"]} for t in payload.get("tools", [])],
|
|
112
|
+
key=lambda x: x["name"],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
99
116
|
def _diff(label: str, local: list[Any], remote: list[Any]) -> int:
|
|
100
117
|
if local == remote:
|
|
101
118
|
print(f"✅ {label}: {len(local)} entries match")
|
|
@@ -129,8 +146,9 @@ def main() -> int:
|
|
|
129
146
|
failed += _diff("resources/list", local_r, remote_r)
|
|
130
147
|
|
|
131
148
|
try:
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
local_t = _normalize_tools(_local_tools_list())
|
|
150
|
+
remote_t = _normalize_tools(_rpc(args.target, "tools/list"))
|
|
151
|
+
failed += _diff("tools/list", local_t, remote_t)
|
|
134
152
|
except Exception as e:
|
|
135
153
|
print(f"❌ tools/list: {e}")
|
|
136
154
|
failed += 1
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Consumer tool catalog — source of truth for Phase 1 discovery stubs.
|
|
2
|
+
|
|
3
|
+
Loaded once at module import from ``consumer_tool_catalog.json``. Both
|
|
4
|
+
the stdio server (``tools.py``) and the cloud pack
|
|
5
|
+
(``scripts/pack_mcp_content.py``) read from this file so the manifest
|
|
6
|
+
returned by ``tools/list`` on either transport is byte-identical apart
|
|
7
|
+
from per-tool ``implemented_on`` metadata.
|
|
8
|
+
|
|
9
|
+
Side-effect classification (``ro`` / ``fs-write`` / ``shell``) and the
|
|
10
|
+
``not_implemented`` envelope contract live in
|
|
11
|
+
``docs/contracts/mcp-tool-stub-envelope.md``.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
_CATALOG_FILE = Path(__file__).resolve().parent / "consumer_tool_catalog.json"
|
|
21
|
+
|
|
22
|
+
# Stable error code surfaced in the ``not_implemented`` envelope. The
|
|
23
|
+
# Worker mirrors this string verbatim — keep them in sync via the
|
|
24
|
+
# envelope contract.
|
|
25
|
+
NOT_IMPLEMENTED_CODE = "not_implemented"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class CatalogEntry:
|
|
30
|
+
"""One row in ``consumer_tool_catalog.json``.
|
|
31
|
+
|
|
32
|
+
``implemented_on`` lists transports where a real handler is wired
|
|
33
|
+
(``stdio`` / ``worker``); missing transports return the
|
|
34
|
+
``not_implemented`` envelope.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
description: str
|
|
39
|
+
side_effect: str
|
|
40
|
+
implemented_on: tuple[str, ...]
|
|
41
|
+
input_schema: dict[str, Any]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _validate(raw: dict[str, Any]) -> None:
|
|
45
|
+
"""Refuse to boot on a malformed catalog. Boot-time errors only."""
|
|
46
|
+
if raw.get("schema_version") != 1:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"catalog: unsupported schema_version="
|
|
49
|
+
f"{raw.get('schema_version')!r}; expected 1"
|
|
50
|
+
)
|
|
51
|
+
tools = raw.get("tools")
|
|
52
|
+
if not isinstance(tools, list) or not tools:
|
|
53
|
+
raise ValueError("catalog: 'tools' must be a non-empty list")
|
|
54
|
+
seen: set[str] = set()
|
|
55
|
+
for entry in tools:
|
|
56
|
+
if not isinstance(entry, dict):
|
|
57
|
+
raise ValueError("catalog: every tool entry must be an object")
|
|
58
|
+
for field in ("name", "description", "side_effect", "input_schema"):
|
|
59
|
+
if field not in entry:
|
|
60
|
+
raise ValueError(f"catalog: tool missing '{field}'")
|
|
61
|
+
if entry["side_effect"] not in ("ro", "fs-write", "shell"):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"catalog: tool {entry['name']!r} has invalid side_effect "
|
|
64
|
+
f"{entry['side_effect']!r} (expected ro / fs-write / shell)"
|
|
65
|
+
)
|
|
66
|
+
name = entry["name"]
|
|
67
|
+
if name in seen:
|
|
68
|
+
raise ValueError(f"catalog: duplicate tool name {name!r}")
|
|
69
|
+
seen.add(name)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load_catalog(path: Path | None = None) -> list[CatalogEntry]:
|
|
73
|
+
"""Parse and validate the catalog. Returns entries in file order."""
|
|
74
|
+
target = path or _CATALOG_FILE
|
|
75
|
+
raw = json.loads(target.read_text(encoding="utf-8"))
|
|
76
|
+
_validate(raw)
|
|
77
|
+
return [
|
|
78
|
+
CatalogEntry(
|
|
79
|
+
name=t["name"],
|
|
80
|
+
description=t["description"],
|
|
81
|
+
side_effect=t["side_effect"],
|
|
82
|
+
implemented_on=tuple(t.get("implemented_on") or ()),
|
|
83
|
+
input_schema=t["input_schema"],
|
|
84
|
+
)
|
|
85
|
+
for t in raw["tools"]
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def load_raw(path: Path | None = None) -> dict[str, Any]:
|
|
90
|
+
"""Return the raw parsed JSON. Used by the cloud packer."""
|
|
91
|
+
target = path or _CATALOG_FILE
|
|
92
|
+
raw = json.loads(target.read_text(encoding="utf-8"))
|
|
93
|
+
_validate(raw)
|
|
94
|
+
return raw
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def install_hint(raw: dict[str, Any] | None = None) -> str:
|
|
98
|
+
"""Stable install-hint surfaced in the envelope."""
|
|
99
|
+
data = raw if raw is not None else load_raw()
|
|
100
|
+
return str(data.get("install_hint_stdio") or "")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def not_implemented_envelope(
|
|
104
|
+
tool_name: str,
|
|
105
|
+
*,
|
|
106
|
+
transport: str,
|
|
107
|
+
install_hint_value: str,
|
|
108
|
+
) -> dict[str, Any]:
|
|
109
|
+
"""Wire-shape error envelope used when a stub is invoked.
|
|
110
|
+
|
|
111
|
+
Mirrored verbatim by the Cloud Worker (`workers/mcp/src/stubs.ts`).
|
|
112
|
+
"""
|
|
113
|
+
return {
|
|
114
|
+
"code": NOT_IMPLEMENTED_CODE,
|
|
115
|
+
"tool": tool_name,
|
|
116
|
+
"transport": transport,
|
|
117
|
+
"install_hint": install_hint_value,
|
|
118
|
+
"alternative": "stdio",
|
|
119
|
+
"message": (
|
|
120
|
+
f"Tool '{tool_name}' is in the discovery catalog but not "
|
|
121
|
+
f"implemented on the {transport} transport. See the install "
|
|
122
|
+
"hint to wire it up locally, or check "
|
|
123
|
+
"docs/contracts/mcp-tool-stub-envelope.md."
|
|
124
|
+
),
|
|
125
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"description": "Source-of-truth catalog of consumer-relevant MCP tools. Read by the stdio server (scripts/mcp_server/) and packed into the Cloud Worker bundle (workers/mcp/). Phase 1 of road-to-mcp-full-coverage: tools without 'implemented' transports return the 'not_implemented' envelope defined in docs/contracts/mcp-tool-stub-envelope.md. The 'implemented_on' field lists transports where the real handler is wired; everything else is a discovery stub. See agents/roadmaps/road-to-mcp-full-coverage.md.",
|
|
4
|
+
"install_hint_stdio": "pip install agent-config[mcp] && ./agent-config mcp:run",
|
|
5
|
+
"tools": [
|
|
6
|
+
{
|
|
7
|
+
"name": "lint_skills",
|
|
8
|
+
"description": "Lint skill / rule / command / guideline / persona markdown files. Returns the same JSON payload as `scripts/skill_linter.py --format json`. Read-only — never writes or spawns git. Pass `paths` to lint a subset, omit for a full tree scan.",
|
|
9
|
+
"side_effect": "ro",
|
|
10
|
+
"implemented_on": ["stdio"],
|
|
11
|
+
"input_schema": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"paths": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": {"type": "string"},
|
|
17
|
+
"description": "Repo-relative paths to lint. Empty / missing → full tree scan."
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"additionalProperties": false
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "chat_history_append",
|
|
25
|
+
"description": "Append one entry to the consumer's chat-history JSONL (`agents/.agent-chat-history`). Path-scoped — writes outside the allowlist raise ValueError. Use `dry_run` to preview the payload without touching the filesystem.",
|
|
26
|
+
"side_effect": "fs-write",
|
|
27
|
+
"implemented_on": ["stdio"],
|
|
28
|
+
"input_schema": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"text": {"type": "string"},
|
|
32
|
+
"entry_type": {"type": "string", "description": "Short `t` tag (e.g. note, decision). Defaults to `note`."},
|
|
33
|
+
"path": {"type": "string", "description": "Optional path override. Must resolve to `agents/.agent-chat-history` or `.agent-chat-history` under consumer_root."},
|
|
34
|
+
"session": {"type": "string"},
|
|
35
|
+
"dry_run": {"type": "boolean", "default": false},
|
|
36
|
+
"min_schema_version": {"type": "integer"}
|
|
37
|
+
},
|
|
38
|
+
"required": ["text"],
|
|
39
|
+
"additionalProperties": false
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "chat_history_read",
|
|
44
|
+
"description": "Read recent chat-history entries from `agents/.agent-chat-history`. Filter by session, limit, or entry-type. Read-only.",
|
|
45
|
+
"side_effect": "ro",
|
|
46
|
+
"implemented_on": ["stdio"],
|
|
47
|
+
"input_schema": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": {
|
|
50
|
+
"last": {"type": "integer", "description": "Return only the last N entries.", "minimum": 1},
|
|
51
|
+
"session": {"type": "string", "description": "Filter by 16-char session id."},
|
|
52
|
+
"entry_type": {"type": "string", "description": "Filter by `t` field."},
|
|
53
|
+
"path": {"type": "string"}
|
|
54
|
+
},
|
|
55
|
+
"additionalProperties": false
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "memory_lookup",
|
|
60
|
+
"description": "Hybrid memory retrieval over `agents/memory/<type>/*.yml` and `agents/memory/intake/*.jsonl`. Read-only.",
|
|
61
|
+
"side_effect": "ro",
|
|
62
|
+
"implemented_on": ["stdio"],
|
|
63
|
+
"input_schema": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"types": {"type": "array", "items": {"type": "string"}, "description": "Memory types to scan (e.g. ownership, historical-patterns)."},
|
|
67
|
+
"keys": {"type": "array", "items": {"type": "string"}, "description": "Path / glob keys to match."},
|
|
68
|
+
"limit": {"type": "integer", "minimum": 1, "default": 5}
|
|
69
|
+
},
|
|
70
|
+
"required": ["types"],
|
|
71
|
+
"additionalProperties": false
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "memory_signal",
|
|
76
|
+
"description": "Append an engineering-memory signal to `agents/memory/intake/signals-YYYY-MM.jsonl`. Rate-limited per `(type, path)` within a rolling window.",
|
|
77
|
+
"side_effect": "fs-write",
|
|
78
|
+
"implemented_on": [],
|
|
79
|
+
"input_schema": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"type": {"type": "string", "description": "Memory type (historical-patterns, incident-learnings, ownership, ...)."},
|
|
83
|
+
"path": {"type": "string", "description": "Anchor path the signal is about."},
|
|
84
|
+
"body": {"type": "string", "description": "Free-form signal body."}
|
|
85
|
+
},
|
|
86
|
+
"required": ["type", "path", "body"],
|
|
87
|
+
"additionalProperties": false
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "memory_status",
|
|
92
|
+
"description": "Report whether the optional `@event4u/agent-memory` package is reachable, and surface its routing metadata. Read-only.",
|
|
93
|
+
"side_effect": "ro",
|
|
94
|
+
"implemented_on": ["stdio"],
|
|
95
|
+
"input_schema": {"type": "object", "properties": {}, "additionalProperties": false}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "skill_trigger_eval",
|
|
99
|
+
"description": "Score a user message against the compiled router and return the matching skills with their trigger reasons. Read-only.",
|
|
100
|
+
"side_effect": "ro",
|
|
101
|
+
"implemented_on": [],
|
|
102
|
+
"input_schema": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"properties": {
|
|
105
|
+
"message": {"type": "string"},
|
|
106
|
+
"context": {"type": "string", "description": "Optional recent-turn context."},
|
|
107
|
+
"limit": {"type": "integer", "minimum": 1, "default": 5}
|
|
108
|
+
},
|
|
109
|
+
"required": ["message"],
|
|
110
|
+
"additionalProperties": false
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "suggest_command",
|
|
115
|
+
"description": "Run the command-suggestion engine: score commands against a user message + context and return the numbered options block. Read-only, deterministic.",
|
|
116
|
+
"side_effect": "ro",
|
|
117
|
+
"implemented_on": [],
|
|
118
|
+
"input_schema": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {
|
|
121
|
+
"message": {"type": "string"},
|
|
122
|
+
"context": {"type": "string"}
|
|
123
|
+
},
|
|
124
|
+
"required": ["message"],
|
|
125
|
+
"additionalProperties": false
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "suggest_skill_for_task",
|
|
130
|
+
"description": "Match a free-form task description to the most relevant skills with frontmatter triggers. Read-only.",
|
|
131
|
+
"side_effect": "ro",
|
|
132
|
+
"implemented_on": [],
|
|
133
|
+
"input_schema": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"properties": {
|
|
136
|
+
"task": {"type": "string"},
|
|
137
|
+
"limit": {"type": "integer", "minimum": 1, "default": 5}
|
|
138
|
+
},
|
|
139
|
+
"required": ["task"],
|
|
140
|
+
"additionalProperties": false
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "mine_session",
|
|
145
|
+
"description": "Extract patterns, decisions, and learnings from a chat-history session JSONL. Read-only.",
|
|
146
|
+
"side_effect": "ro",
|
|
147
|
+
"implemented_on": [],
|
|
148
|
+
"input_schema": {
|
|
149
|
+
"type": "object",
|
|
150
|
+
"properties": {
|
|
151
|
+
"session": {"type": "string", "description": "Session id to mine."},
|
|
152
|
+
"path": {"type": "string", "description": "Optional path to a chat-history JSONL."}
|
|
153
|
+
},
|
|
154
|
+
"additionalProperties": false
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "update_form_request_messages",
|
|
159
|
+
"description": "Sync the `messages()` method of a Laravel FormRequest class — add missing entries, link to language keys, drop stale ones.",
|
|
160
|
+
"side_effect": "fs-write",
|
|
161
|
+
"implemented_on": [],
|
|
162
|
+
"input_schema": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"properties": {
|
|
165
|
+
"request_class": {"type": "string", "description": "Fully-qualified FormRequest class FQN or repo-relative file path."},
|
|
166
|
+
"dry_run": {"type": "boolean", "default": false}
|
|
167
|
+
},
|
|
168
|
+
"required": ["request_class"],
|
|
169
|
+
"additionalProperties": false
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "sync_gitignore",
|
|
174
|
+
"description": "Synchronise the `event4u/agent-config` block in the consumer project's `.gitignore` — adds missing entries, preserves user-added lines.",
|
|
175
|
+
"side_effect": "fs-write",
|
|
176
|
+
"implemented_on": [],
|
|
177
|
+
"input_schema": {
|
|
178
|
+
"type": "object",
|
|
179
|
+
"properties": {
|
|
180
|
+
"dry_run": {"type": "boolean", "default": false}
|
|
181
|
+
},
|
|
182
|
+
"additionalProperties": false
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"name": "sync_agent_settings",
|
|
187
|
+
"description": "Synchronise `.agent-settings.yml` against the current template + profile — adds new sections/keys, preserves user values.",
|
|
188
|
+
"side_effect": "fs-write",
|
|
189
|
+
"implemented_on": [],
|
|
190
|
+
"input_schema": {
|
|
191
|
+
"type": "object",
|
|
192
|
+
"properties": {
|
|
193
|
+
"profile": {"type": "string", "description": "Override the profile pinned in the settings file."},
|
|
194
|
+
"dry_run": {"type": "boolean", "default": false}
|
|
195
|
+
},
|
|
196
|
+
"additionalProperties": false
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"name": "run_tests",
|
|
201
|
+
"description": "Execute the consumer project's test suite via the project's standard test runner. Returns the exit code, runner name, and a structured tail of the output.",
|
|
202
|
+
"side_effect": "shell",
|
|
203
|
+
"implemented_on": [],
|
|
204
|
+
"input_schema": {
|
|
205
|
+
"type": "object",
|
|
206
|
+
"properties": {
|
|
207
|
+
"filter": {"type": "string", "description": "Restrict to tests matching this pattern."},
|
|
208
|
+
"path": {"type": "string", "description": "Restrict to tests under this directory."}
|
|
209
|
+
},
|
|
210
|
+
"additionalProperties": false
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "run_quality_checks",
|
|
215
|
+
"description": "Run the configured quality gate (PHPStan / Rector / ECS / equivalent) and return a structured per-tool result.",
|
|
216
|
+
"side_effect": "shell",
|
|
217
|
+
"implemented_on": [],
|
|
218
|
+
"input_schema": {
|
|
219
|
+
"type": "object",
|
|
220
|
+
"properties": {
|
|
221
|
+
"tool": {"type": "string", "description": "Restrict to a single tool name."}
|
|
222
|
+
},
|
|
223
|
+
"additionalProperties": false
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"name": "list_skills",
|
|
228
|
+
"description": "Enumerate every skill currently exposed as a prompt, with name + description + source. Read-only manifest view.",
|
|
229
|
+
"side_effect": "ro",
|
|
230
|
+
"implemented_on": ["stdio"],
|
|
231
|
+
"input_schema": {"type": "object", "properties": {}, "additionalProperties": false}
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"name": "list_commands",
|
|
235
|
+
"description": "Enumerate every slash command currently exposed as a prompt. Read-only manifest view.",
|
|
236
|
+
"side_effect": "ro",
|
|
237
|
+
"implemented_on": ["stdio"],
|
|
238
|
+
"input_schema": {"type": "object", "properties": {}, "additionalProperties": false}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"name": "list_rules",
|
|
242
|
+
"description": "Enumerate every rule currently exposed as a resource. Read-only manifest view.",
|
|
243
|
+
"side_effect": "ro",
|
|
244
|
+
"implemented_on": ["stdio"],
|
|
245
|
+
"input_schema": {"type": "object", "properties": {}, "additionalProperties": false}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "compile_router",
|
|
249
|
+
"description": "Regenerate the compiled router (`router.compiled.json`) from the current rule / skill / command frontmatter. Shell-bound — wraps `scripts/compile_router.py`.",
|
|
250
|
+
"side_effect": "shell",
|
|
251
|
+
"implemented_on": [],
|
|
252
|
+
"input_schema": {
|
|
253
|
+
"type": "object",
|
|
254
|
+
"properties": {
|
|
255
|
+
"dry_run": {"type": "boolean", "default": false}
|
|
256
|
+
},
|
|
257
|
+
"additionalProperties": false
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"name": "read_resource_body",
|
|
262
|
+
"description": "Fetch the rendered body of any resource URI (rule, guideline, context) without going through `resources/read`. Convenience for clients that want to inline content into a tool call result.",
|
|
263
|
+
"side_effect": "ro",
|
|
264
|
+
"implemented_on": ["stdio"],
|
|
265
|
+
"input_schema": {
|
|
266
|
+
"type": "object",
|
|
267
|
+
"properties": {
|
|
268
|
+
"uri": {"type": "string", "description": "Resource URI like `rule://commit-policy`."}
|
|
269
|
+
},
|
|
270
|
+
"required": ["uri"],
|
|
271
|
+
"additionalProperties": false
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
]
|
|
275
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""MCP telemetry sink — Phase 1 J4 instrumentation.
|
|
2
|
+
|
|
3
|
+
Per ``agents/roadmaps/road-to-mcp-full-coverage.md`` §Phase 1 J4 +
|
|
4
|
+
``docs/contracts/mcp-tool-stub-envelope.md``, both transports log every
|
|
5
|
+
``tools/call`` with ``{tool_name, client_id_hash, ts, transport,
|
|
6
|
+
outcome}``. Payload bodies are never logged; the client identifier is
|
|
7
|
+
hashed at the server boundary so the queryable store never sees raw
|
|
8
|
+
identity.
|
|
9
|
+
|
|
10
|
+
Outcomes:
|
|
11
|
+
|
|
12
|
+
- ``implemented`` — real handler ran (no envelope returned).
|
|
13
|
+
- ``stub`` — catalog entry missing this transport; ``not_implemented``
|
|
14
|
+
envelope returned.
|
|
15
|
+
- ``latent_demand`` — caller asked for a tool not in the catalog.
|
|
16
|
+
|
|
17
|
+
The sink writes JSONL to ``agents/.mcp-telemetry/calls.jsonl`` under the
|
|
18
|
+
consumer root. Failure to write must not break the wire surface: the
|
|
19
|
+
``record_call`` helper swallows OSError + ValueError and emits a single
|
|
20
|
+
warning to stderr.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import hashlib
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Literal
|
|
31
|
+
|
|
32
|
+
Outcome = Literal["implemented", "stub", "latent_demand"]
|
|
33
|
+
|
|
34
|
+
# Stable file location relative to consumer_root. Phase 2 K1 routes
|
|
35
|
+
# this into a queryable store; Phase 1 only needs the file to exist.
|
|
36
|
+
TELEMETRY_REL_DIR = "agents/.mcp-telemetry"
|
|
37
|
+
TELEMETRY_FILENAME = "calls.jsonl"
|
|
38
|
+
|
|
39
|
+
# Truncation length for the client_id hash. 12 hex chars = 48 bits of
|
|
40
|
+
# entropy — enough to distinguish hundreds of consumers without
|
|
41
|
+
# becoming a re-identification vector.
|
|
42
|
+
_HASH_LEN = 12
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _client_id_seed() -> str:
|
|
46
|
+
"""Identity components that together pin a consumer install.
|
|
47
|
+
|
|
48
|
+
USER + machine hostname + repo path is a stable triple that survives
|
|
49
|
+
sessions without leaking PII into the log. The hash never reverses.
|
|
50
|
+
"""
|
|
51
|
+
user = os.environ.get("USER") or os.environ.get("USERNAME") or "unknown"
|
|
52
|
+
host = os.environ.get("HOSTNAME")
|
|
53
|
+
if not host and hasattr(os, "uname"):
|
|
54
|
+
host = os.uname().nodename
|
|
55
|
+
host = host or "unknown"
|
|
56
|
+
cwd = str(Path.cwd().resolve())
|
|
57
|
+
return f"{user}|{host}|{cwd}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def hash_client_id(seed: str | None = None) -> str:
|
|
61
|
+
"""SHA-256(seed) truncated to 12 hex chars. Boundary-only call."""
|
|
62
|
+
raw = seed if seed is not None else _client_id_seed()
|
|
63
|
+
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
64
|
+
return digest[:_HASH_LEN]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _resolve_log_path(consumer_root: Path | None = None) -> Path:
|
|
68
|
+
"""Pick the JSONL location. Defaults to CWD when no override given."""
|
|
69
|
+
root = (consumer_root or Path.cwd()).resolve()
|
|
70
|
+
return root / TELEMETRY_REL_DIR / TELEMETRY_FILENAME
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _now_iso() -> str:
|
|
74
|
+
"""ISO-8601 UTC timestamp, seconds precision."""
|
|
75
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_record(
|
|
79
|
+
*,
|
|
80
|
+
tool_name: str,
|
|
81
|
+
outcome: Outcome,
|
|
82
|
+
transport: str,
|
|
83
|
+
client_id_hash_value: str | None = None,
|
|
84
|
+
ts: str | None = None,
|
|
85
|
+
) -> dict[str, object]:
|
|
86
|
+
"""Pure helper — assemble the record without touching the filesystem."""
|
|
87
|
+
return {
|
|
88
|
+
"tool_name": tool_name,
|
|
89
|
+
"client_id_hash": client_id_hash_value or hash_client_id(),
|
|
90
|
+
"ts": ts or _now_iso(),
|
|
91
|
+
"transport": transport,
|
|
92
|
+
"outcome": outcome,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def record_call(
|
|
97
|
+
*,
|
|
98
|
+
tool_name: str,
|
|
99
|
+
outcome: Outcome,
|
|
100
|
+
transport: str,
|
|
101
|
+
consumer_root: Path | None = None,
|
|
102
|
+
client_id_hash_value: str | None = None,
|
|
103
|
+
) -> dict[str, object] | None:
|
|
104
|
+
"""Append one JSONL record. Returns the record or None on failure.
|
|
105
|
+
|
|
106
|
+
Failures are swallowed: telemetry must never break the wire surface.
|
|
107
|
+
A single ``mcp-server: warn: telemetry`` line is emitted to stderr
|
|
108
|
+
so silent-failure windows show up in the boot log and the J6
|
|
109
|
+
healthcheck can detect them.
|
|
110
|
+
"""
|
|
111
|
+
record = build_record(
|
|
112
|
+
tool_name=tool_name,
|
|
113
|
+
outcome=outcome,
|
|
114
|
+
transport=transport,
|
|
115
|
+
client_id_hash_value=client_id_hash_value,
|
|
116
|
+
)
|
|
117
|
+
target = _resolve_log_path(consumer_root)
|
|
118
|
+
try:
|
|
119
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
with target.open("a", encoding="utf-8") as fh:
|
|
121
|
+
fh.write(json.dumps(record, separators=(",", ":")) + "\n")
|
|
122
|
+
except (OSError, ValueError) as exc:
|
|
123
|
+
print(
|
|
124
|
+
f"mcp-server: warn: telemetry write failed: {exc}",
|
|
125
|
+
file=sys.stderr,
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
return record
|