@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,201 @@
|
|
|
1
|
+
"""Resource loader — exposes rules, guidelines, contexts as MCP resources.
|
|
2
|
+
|
|
3
|
+
Phase 3 (C1–C4) extends the read-only MCP surface from prompts (skills
|
|
4
|
+
+ commands) to read-only **resources** for the governance layer:
|
|
5
|
+
|
|
6
|
+
- `rule://<basename>` — `.agent-src/rules/*.md`
|
|
7
|
+
- `guideline://<relpath-no-ext>` — `docs/guidelines/**/*.md`
|
|
8
|
+
- `context://<relpath-no-ext>` — `.agent-src/contexts/**/*.md`
|
|
9
|
+
|
|
10
|
+
All three are served with `mimeType=text/markdown`. The merge-at-sync
|
|
11
|
+
contract is the same as for prompts: `.agent-src/` is already the
|
|
12
|
+
package + project merged view; this loader does not re-merge.
|
|
13
|
+
|
|
14
|
+
Description resolution: frontmatter `description:` wins, else the
|
|
15
|
+
first H1 line (`# Title`) is used as a title-style fallback, else the
|
|
16
|
+
filename-derived stem.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Literal
|
|
24
|
+
|
|
25
|
+
from .prompts import _project_root, _strip_frontmatter
|
|
26
|
+
|
|
27
|
+
ResourceKind = Literal["rule", "guideline", "context"]
|
|
28
|
+
MIME_MARKDOWN = "text/markdown"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Resource:
|
|
33
|
+
"""Resolved Markdown asset ready for MCP exposure."""
|
|
34
|
+
|
|
35
|
+
uri: str
|
|
36
|
+
name: str
|
|
37
|
+
description: str
|
|
38
|
+
body: str
|
|
39
|
+
source: str = "package"
|
|
40
|
+
mime_type: str = MIME_MARKDOWN
|
|
41
|
+
kind: ResourceKind = "rule"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_H1_RE = re.compile(r"^#\s+(.+?)\s*$", re.MULTILINE)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _derive_description(meta: dict[str, str], body: str, fallback: str) -> str:
|
|
48
|
+
desc = meta.get("description", "").strip()
|
|
49
|
+
if desc:
|
|
50
|
+
return desc
|
|
51
|
+
match = _H1_RE.search(body)
|
|
52
|
+
if match:
|
|
53
|
+
return match.group(1).strip()
|
|
54
|
+
return fallback
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _load(path: Path, *, uri: str, fallback_name: str, kind: ResourceKind) -> Resource:
|
|
58
|
+
text = path.read_text(encoding="utf-8")
|
|
59
|
+
meta, body = _strip_frontmatter(text)
|
|
60
|
+
name = meta.get("name", fallback_name).strip() or fallback_name
|
|
61
|
+
description = _derive_description(meta, body, fallback_name)
|
|
62
|
+
return Resource(
|
|
63
|
+
uri=uri,
|
|
64
|
+
name=name,
|
|
65
|
+
description=description,
|
|
66
|
+
body=text.rstrip() + "\n",
|
|
67
|
+
source=meta.get("source", "package"),
|
|
68
|
+
kind=kind,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def scan_rules(root: Path | None = None) -> tuple[list[Resource], list[str]]:
|
|
73
|
+
base = root or _project_root()
|
|
74
|
+
rules_root = base / ".agent-src" / "rules"
|
|
75
|
+
out: list[Resource] = []
|
|
76
|
+
errors: list[str] = []
|
|
77
|
+
if not rules_root.is_dir():
|
|
78
|
+
return out, errors
|
|
79
|
+
for path in sorted(rules_root.glob("*.md")):
|
|
80
|
+
if not path.is_file():
|
|
81
|
+
continue
|
|
82
|
+
stem = path.stem
|
|
83
|
+
try:
|
|
84
|
+
out.append(_load(path, uri=f"rule://{stem}", fallback_name=stem, kind="rule"))
|
|
85
|
+
except OSError as exc:
|
|
86
|
+
errors.append(f"{path}: read failed ({exc})")
|
|
87
|
+
return out, errors
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _scan_tree(
|
|
91
|
+
root: Path,
|
|
92
|
+
*,
|
|
93
|
+
scheme: str,
|
|
94
|
+
kind: ResourceKind,
|
|
95
|
+
) -> tuple[list[Resource], list[str]]:
|
|
96
|
+
out: list[Resource] = []
|
|
97
|
+
errors: list[str] = []
|
|
98
|
+
if not root.is_dir():
|
|
99
|
+
return out, errors
|
|
100
|
+
for path in sorted(root.rglob("*.md")):
|
|
101
|
+
if not path.is_file():
|
|
102
|
+
continue
|
|
103
|
+
rel = path.relative_to(root).with_suffix("")
|
|
104
|
+
slug = str(rel).replace("\\", "/")
|
|
105
|
+
try:
|
|
106
|
+
out.append(
|
|
107
|
+
_load(path, uri=f"{scheme}://{slug}", fallback_name=slug, kind=kind)
|
|
108
|
+
)
|
|
109
|
+
except OSError as exc:
|
|
110
|
+
errors.append(f"{path}: read failed ({exc})")
|
|
111
|
+
return out, errors
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def scan_guidelines(root: Path | None = None) -> tuple[list[Resource], list[str]]:
|
|
115
|
+
base = root or _project_root()
|
|
116
|
+
return _scan_tree(base / "docs" / "guidelines", scheme="guideline", kind="guideline")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def scan_contexts(root: Path | None = None) -> tuple[list[Resource], list[str]]:
|
|
120
|
+
base = root or _project_root()
|
|
121
|
+
return _scan_tree(base / ".agent-src" / "contexts", scheme="context", kind="context")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def load_all_resources(
|
|
125
|
+
root: Path | None = None,
|
|
126
|
+
) -> tuple[list[Resource], list[str]]:
|
|
127
|
+
"""Phase 3 entrypoint — every rule, guideline, context."""
|
|
128
|
+
rules, e1 = scan_rules(root)
|
|
129
|
+
guidelines, e2 = scan_guidelines(root)
|
|
130
|
+
contexts, e3 = scan_contexts(root)
|
|
131
|
+
errors = list(e1) + list(e2) + list(e3)
|
|
132
|
+
seen: dict[str, Resource] = {}
|
|
133
|
+
for r in rules + guidelines + contexts:
|
|
134
|
+
if r.uri in seen:
|
|
135
|
+
errors.append(f"duplicate URI {r.uri!r}: keeping first")
|
|
136
|
+
continue
|
|
137
|
+
seen[r.uri] = r
|
|
138
|
+
merged = sorted(seen.values(), key=lambda r: r.uri)
|
|
139
|
+
return merged, errors
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def to_mcp_resource_meta(resource: Resource) -> dict[str, object]:
|
|
143
|
+
"""Project a Resource into MCP `Resource` constructor kwargs."""
|
|
144
|
+
return {
|
|
145
|
+
"uri": resource.uri,
|
|
146
|
+
"name": resource.name,
|
|
147
|
+
"description": resource.description,
|
|
148
|
+
"mimeType": resource.mime_type,
|
|
149
|
+
"_meta": {"source": resource.source, "kind": resource.kind},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ResourceCache:
|
|
154
|
+
"""In-memory cache with mtime-based invalidation (mirrors `PromptCache`).
|
|
155
|
+
|
|
156
|
+
Re-scans rules / guidelines / contexts on each `get()` when the set
|
|
157
|
+
of tracked files or any mtime has changed. No watcher dependency.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def __init__(self, root: Path | None = None) -> None:
|
|
161
|
+
self._root = root or _project_root()
|
|
162
|
+
self._resources: list[Resource] = []
|
|
163
|
+
self._errors: list[str] = []
|
|
164
|
+
self._signature: tuple[tuple[str, float], ...] = ()
|
|
165
|
+
self._index: dict[str, Resource] = {}
|
|
166
|
+
|
|
167
|
+
def _current_signature(self) -> tuple[tuple[str, float], ...]:
|
|
168
|
+
entries: list[tuple[str, float]] = []
|
|
169
|
+
for sub in (
|
|
170
|
+
self._root / ".agent-src" / "rules",
|
|
171
|
+
self._root / "docs" / "guidelines",
|
|
172
|
+
self._root / ".agent-src" / "contexts",
|
|
173
|
+
):
|
|
174
|
+
if not sub.is_dir():
|
|
175
|
+
continue
|
|
176
|
+
for path in sorted(sub.rglob("*.md")):
|
|
177
|
+
if path.is_file():
|
|
178
|
+
entries.append((str(path), path.stat().st_mtime))
|
|
179
|
+
return tuple(entries)
|
|
180
|
+
|
|
181
|
+
def _refresh(self) -> None:
|
|
182
|
+
resources, errors = load_all_resources(self._root)
|
|
183
|
+
self._resources = resources
|
|
184
|
+
self._errors = errors
|
|
185
|
+
self._index = {r.uri: r for r in resources}
|
|
186
|
+
|
|
187
|
+
def get(self) -> tuple[list[Resource], list[str]]:
|
|
188
|
+
signature = self._current_signature()
|
|
189
|
+
if signature != self._signature:
|
|
190
|
+
self._signature = signature
|
|
191
|
+
self._refresh()
|
|
192
|
+
return self._resources, self._errors
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def signature(self) -> tuple[tuple[str, float], ...]:
|
|
196
|
+
"""Cached `(path, mtime)` tuples (Phase-6 F1 input). Call `get()` first."""
|
|
197
|
+
return self._signature
|
|
198
|
+
|
|
199
|
+
def lookup(self, uri: str) -> Resource | None:
|
|
200
|
+
self.get()
|
|
201
|
+
return self._index.get(uri)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""MCP Server — registers `prompts/*` + `resources/*` over stdio.
|
|
2
|
+
|
|
3
|
+
Phase 3 boundary (A0 Hard Contract still holds): read-only. No
|
|
4
|
+
`tools/*`, no filesystem writes. New in Phase 3:
|
|
5
|
+
|
|
6
|
+
- **C1/C2** `resources/list` + `resources/read` for rules,
|
|
7
|
+
guidelines, contexts via `ResourceCache`.
|
|
8
|
+
- **C3** cursor-based pagination on `resources/list` (same shape as
|
|
9
|
+
prompts/list).
|
|
10
|
+
- **C4** hot-reload — `ResourceCache` re-scans on mtime change before
|
|
11
|
+
each `resources/list` response.
|
|
12
|
+
|
|
13
|
+
Carried over from Phase 2:
|
|
14
|
+
|
|
15
|
+
- **B1/B2** full skills + commands coverage via `PromptCache`.
|
|
16
|
+
- **B4** cursor-based pagination on `prompts/list`.
|
|
17
|
+
- **B5** hot-reload — `PromptCache` re-scans on mtime change before
|
|
18
|
+
each `prompts/list` response.
|
|
19
|
+
|
|
20
|
+
`build_server` still accepts a plain `list[SkillPrompt]` so the
|
|
21
|
+
Phase-1 contract tests keep passing without touching their fixtures.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import sys
|
|
27
|
+
from typing import Callable, Iterable, Union
|
|
28
|
+
|
|
29
|
+
import mcp.types as types
|
|
30
|
+
from mcp.server import NotificationOptions, Server
|
|
31
|
+
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
32
|
+
from mcp.server.models import InitializationOptions
|
|
33
|
+
from mcp.server.stdio import stdio_server
|
|
34
|
+
from pydantic import AnyUrl
|
|
35
|
+
|
|
36
|
+
from . import SERVER_NAME, __version__
|
|
37
|
+
from .metadata import (
|
|
38
|
+
boot_log_line as identity_boot_log_line,
|
|
39
|
+
compute_skill_set_signature,
|
|
40
|
+
read_package_version,
|
|
41
|
+
)
|
|
42
|
+
from .prompts import (
|
|
43
|
+
PromptCache,
|
|
44
|
+
SkillPrompt,
|
|
45
|
+
_project_root,
|
|
46
|
+
to_mcp_prompt_meta,
|
|
47
|
+
)
|
|
48
|
+
from .resources import (
|
|
49
|
+
Resource,
|
|
50
|
+
ResourceCache,
|
|
51
|
+
to_mcp_resource_meta,
|
|
52
|
+
)
|
|
53
|
+
from .tools import (
|
|
54
|
+
ToolCache,
|
|
55
|
+
boot_log_line as tools_boot_log_line,
|
|
56
|
+
to_mcp_tool_meta,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Page size for cursor-based pagination. Conservative default —
|
|
60
|
+
# Claude Desktop and Zed handle larger pages, but small pages keep
|
|
61
|
+
# wire payloads under typical stdio frame limits.
|
|
62
|
+
DEFAULT_PAGE_SIZE = 100
|
|
63
|
+
|
|
64
|
+
PromptsSource = Union[
|
|
65
|
+
list[SkillPrompt],
|
|
66
|
+
Callable[[], tuple[list[SkillPrompt], list[str]]],
|
|
67
|
+
]
|
|
68
|
+
ResourcesSource = Union[
|
|
69
|
+
list[Resource],
|
|
70
|
+
Callable[[], tuple[list[Resource], list[str]]],
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _make_loader(
|
|
75
|
+
source: PromptsSource,
|
|
76
|
+
) -> Callable[[], tuple[list[SkillPrompt], list[str]]]:
|
|
77
|
+
"""Normalise to a callable returning `(prompts, errors)`."""
|
|
78
|
+
if callable(source):
|
|
79
|
+
return source
|
|
80
|
+
static = list(source)
|
|
81
|
+
return lambda: (static, [])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _make_resource_loader(
|
|
85
|
+
source: ResourcesSource,
|
|
86
|
+
) -> Callable[[], tuple[list[Resource], list[str]]]:
|
|
87
|
+
"""Normalise to a callable returning `(resources, errors)`."""
|
|
88
|
+
if callable(source):
|
|
89
|
+
return source
|
|
90
|
+
static = list(source)
|
|
91
|
+
return lambda: (static, [])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _decode_cursor(cursor: str | None, total: int) -> int:
|
|
95
|
+
"""Cursor is a stringified integer offset. Invalid → start at 0."""
|
|
96
|
+
if cursor is None:
|
|
97
|
+
return 0
|
|
98
|
+
try:
|
|
99
|
+
offset = int(cursor)
|
|
100
|
+
except (TypeError, ValueError):
|
|
101
|
+
return 0
|
|
102
|
+
if offset < 0 or offset > total:
|
|
103
|
+
return 0
|
|
104
|
+
return offset
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def build_server(
|
|
108
|
+
source: PromptsSource,
|
|
109
|
+
*,
|
|
110
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
111
|
+
resources: ResourcesSource | None = None,
|
|
112
|
+
tools: ToolCache | None = None,
|
|
113
|
+
) -> Server:
|
|
114
|
+
"""Construct the MCP Server with the new-style paginated handlers.
|
|
115
|
+
|
|
116
|
+
Pure factory — no I/O. Tests pass a static list; the stdio
|
|
117
|
+
entrypoint passes a `PromptCache.get` callable for hot-reload.
|
|
118
|
+
When `resources` is omitted, resources/* handlers are still
|
|
119
|
+
registered but return an empty list — clients can probe the
|
|
120
|
+
capability without seeing a protocol error.
|
|
121
|
+
"""
|
|
122
|
+
loader = _make_loader(source)
|
|
123
|
+
resource_loader = _make_resource_loader(resources or [])
|
|
124
|
+
server: Server = Server(
|
|
125
|
+
name=SERVER_NAME,
|
|
126
|
+
version=__version__,
|
|
127
|
+
instructions=(
|
|
128
|
+
"agent-config MCP server (Phase 3, experimental). Exposes "
|
|
129
|
+
"all skills + commands as instructional prompts, plus "
|
|
130
|
+
"rules + guidelines + contexts as read-only resources."
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@server.list_prompts()
|
|
135
|
+
async def _list_prompts(
|
|
136
|
+
req: types.ListPromptsRequest,
|
|
137
|
+
) -> types.ListPromptsResult:
|
|
138
|
+
prompts, _errors = loader()
|
|
139
|
+
cursor = req.params.cursor if req.params else None
|
|
140
|
+
start = _decode_cursor(cursor, len(prompts))
|
|
141
|
+
end = start + page_size
|
|
142
|
+
page = prompts[start:end]
|
|
143
|
+
next_cursor: str | None = str(end) if end < len(prompts) else None
|
|
144
|
+
return types.ListPromptsResult(
|
|
145
|
+
prompts=[types.Prompt(**to_mcp_prompt_meta(p)) for p in page],
|
|
146
|
+
nextCursor=next_cursor,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@server.get_prompt()
|
|
150
|
+
async def _get_prompt(
|
|
151
|
+
name: str,
|
|
152
|
+
arguments: dict[str, str] | None = None,
|
|
153
|
+
) -> types.GetPromptResult:
|
|
154
|
+
prompts, _errors = loader()
|
|
155
|
+
index = {to_mcp_prompt_meta(p)["name"]: p for p in prompts}
|
|
156
|
+
prompt = index.get(name)
|
|
157
|
+
if prompt is None:
|
|
158
|
+
raise ValueError(f"Unknown prompt: {name}")
|
|
159
|
+
return types.GetPromptResult(
|
|
160
|
+
description=prompt.description,
|
|
161
|
+
messages=[
|
|
162
|
+
types.PromptMessage(
|
|
163
|
+
role="user",
|
|
164
|
+
content=types.TextContent(
|
|
165
|
+
type="text",
|
|
166
|
+
text=prompt.body,
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@server.list_resources()
|
|
173
|
+
async def _list_resources(
|
|
174
|
+
req: types.ListResourcesRequest,
|
|
175
|
+
) -> types.ListResourcesResult:
|
|
176
|
+
items, _errors = resource_loader()
|
|
177
|
+
cursor = req.params.cursor if req.params else None
|
|
178
|
+
start = _decode_cursor(cursor, len(items))
|
|
179
|
+
end = start + page_size
|
|
180
|
+
page = items[start:end]
|
|
181
|
+
next_cursor: str | None = str(end) if end < len(items) else None
|
|
182
|
+
return types.ListResourcesResult(
|
|
183
|
+
resources=[types.Resource(**to_mcp_resource_meta(r)) for r in page],
|
|
184
|
+
nextCursor=next_cursor,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@server.read_resource()
|
|
188
|
+
async def _read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]:
|
|
189
|
+
items, _errors = resource_loader()
|
|
190
|
+
index = {r.uri: r for r in items}
|
|
191
|
+
resource = index.get(str(uri))
|
|
192
|
+
if resource is None:
|
|
193
|
+
raise ValueError(f"Unknown resource: {uri}")
|
|
194
|
+
return [
|
|
195
|
+
ReadResourceContents(content=resource.body, mime_type=resource.mime_type),
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
if tools is not None:
|
|
199
|
+
tool_cache = tools
|
|
200
|
+
|
|
201
|
+
@server.list_tools()
|
|
202
|
+
async def _list_tools() -> list[types.Tool]:
|
|
203
|
+
return [types.Tool(**to_mcp_tool_meta(t)) for t in tool_cache.list()]
|
|
204
|
+
|
|
205
|
+
@server.call_tool()
|
|
206
|
+
async def _call_tool(
|
|
207
|
+
name: str,
|
|
208
|
+
arguments: dict[str, object],
|
|
209
|
+
) -> dict[str, object]:
|
|
210
|
+
return await tool_cache.dispatch(name, arguments or {})
|
|
211
|
+
|
|
212
|
+
return server
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def run_stdio() -> None:
|
|
216
|
+
"""Entrypoint — load prompts + resources via caches, run server over stdio."""
|
|
217
|
+
cache = PromptCache()
|
|
218
|
+
prompts, errors = cache.get()
|
|
219
|
+
for line in errors:
|
|
220
|
+
print(f"mcp-server: warn: {line}", file=sys.stderr)
|
|
221
|
+
print(
|
|
222
|
+
f"mcp-server: loaded {len(prompts)} prompts "
|
|
223
|
+
f"({len(errors)} warnings)",
|
|
224
|
+
file=sys.stderr,
|
|
225
|
+
)
|
|
226
|
+
resource_cache = ResourceCache()
|
|
227
|
+
resources_list, resource_errors = resource_cache.get()
|
|
228
|
+
for line in resource_errors:
|
|
229
|
+
print(f"mcp-server: warn: {line}", file=sys.stderr)
|
|
230
|
+
print(
|
|
231
|
+
f"mcp-server: loaded {len(resources_list)} resources "
|
|
232
|
+
f"({len(resource_errors)} warnings)",
|
|
233
|
+
file=sys.stderr,
|
|
234
|
+
)
|
|
235
|
+
tool_cache = ToolCache()
|
|
236
|
+
print(tools_boot_log_line(tool_cache), file=sys.stderr)
|
|
237
|
+
package_version = read_package_version(_project_root())
|
|
238
|
+
skill_set_signature = compute_skill_set_signature(
|
|
239
|
+
cache.signature,
|
|
240
|
+
resource_cache.signature,
|
|
241
|
+
)
|
|
242
|
+
print(
|
|
243
|
+
identity_boot_log_line(
|
|
244
|
+
server_version=__version__,
|
|
245
|
+
package_version=package_version,
|
|
246
|
+
skill_set_signature=skill_set_signature,
|
|
247
|
+
),
|
|
248
|
+
file=sys.stderr,
|
|
249
|
+
)
|
|
250
|
+
server = build_server(
|
|
251
|
+
cache.get,
|
|
252
|
+
resources=resource_cache.get,
|
|
253
|
+
tools=tool_cache,
|
|
254
|
+
)
|
|
255
|
+
init_options = InitializationOptions(
|
|
256
|
+
server_name=SERVER_NAME,
|
|
257
|
+
server_version=__version__,
|
|
258
|
+
capabilities=server.get_capabilities(
|
|
259
|
+
notification_options=NotificationOptions(),
|
|
260
|
+
experimental_capabilities={},
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
264
|
+
await server.run(read_stream, write_stream, init_options)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main() -> None:
|
|
268
|
+
"""Sync wrapper for `python -m scripts.mcp_server`."""
|
|
269
|
+
asyncio.run(run_stdio())
|