@adrkit/mcp 0.2.0 → 0.3.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/README.md +292 -31
- package/dist/bin.js +50 -70
- package/dist/corpus/ordering.d.ts +6 -15
- package/dist/index.d.ts +24 -2
- package/dist/index.js +42 -69
- package/dist/main-module.d.ts +9 -0
- package/dist/server.d.ts +17 -3
- package/dist/tools/get-decision-context.d.ts +1 -1
- package/dist/tools/get-decision.d.ts +1 -1
- package/dist/tools/list-superseded.d.ts +1 -1
- package/dist/tools/search-decisions.d.ts +1 -1
- package/dist/tools/shared.d.ts +2 -2
- package/package.json +6 -4
- package/src/corpus/ordering.ts +13 -38
- package/src/corpus/projection.ts +8 -27
- package/src/index.ts +35 -20
- package/src/main-module.ts +20 -1
- package/src/server.ts +38 -5
- package/src/tools/get-decision-context.ts +7 -5
- package/src/tools/get-decision.ts +1 -1
- package/src/tools/list-superseded.ts +1 -1
- package/src/tools/search-decisions.ts +1 -1
- package/src/tools/shared.ts +10 -10
package/README.md
CHANGED
|
@@ -1,22 +1,152 @@
|
|
|
1
1
|
# `@adrkit/mcp`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
ask "has this been decided?", "what governs these files?", and "what replaced this?"
|
|
6
|
-
against one Git-backed ADR corpus — deterministically, offline, with no model,
|
|
7
|
-
network, or write access.
|
|
3
|
+
**Deterministic, offline, read-only decision memory for coding agents — including
|
|
4
|
+
the decisions you already rejected.**
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
A local [Model Context Protocol](https://modelcontextprotocol.io) server that lets
|
|
7
|
+
an agent harness query one Git-backed ADR (Architecture Decision Record) corpus
|
|
8
|
+
over stdio: *"has this been decided?"*, *"what governs these files?"*, and *"what
|
|
9
|
+
replaced this?"* It surfaces **superseded and rejected** decisions specifically so
|
|
10
|
+
agents stop re-proposing paths the team already ruled out.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
> **No model calls. No network calls. No writes.** The server reads Markdown files
|
|
13
|
+
> from your repo and returns structured JSON. It never calls an LLM, never opens a
|
|
14
|
+
> socket, and never mutates your corpus. Every tool is annotated
|
|
15
|
+
> `readOnlyHint: true`, `openWorldHint: false`.
|
|
16
|
+
|
|
17
|
+
Part of [adrkit](https://adrkit.dev). The corpus lives in git as one Markdown file
|
|
18
|
+
per decision with typed YAML frontmatter (`@adrkit/core`); this server only reads
|
|
19
|
+
it. Listed in the [official MCP registry](https://registry.modelcontextprotocol.io)
|
|
20
|
+
as **`dev.adrkit/mcp`** (this is the Node/npm `@adrkit/mcp` package — unrelated to
|
|
21
|
+
the `adr-kit` Python package on PyPI). A registry listing is distribution, not
|
|
22
|
+
adoption; see Maturity below.
|
|
23
|
+
|
|
24
|
+
## Maturity
|
|
25
|
+
|
|
26
|
+
adrkit is **early**. Phases 0–6 are *landed / reference-verified* against
|
|
27
|
+
[ADR-0014](https://github.com/mbeacom/adrkit/blob/main/docs/adr/0014-stage-phase-landing-evidence-across-a-three-rung-validation-ladder.md)
|
|
28
|
+
rungs 1–2 (unit/contract/conformance plus maintainer-owned isolated
|
|
29
|
+
reference-repository validation). It has **no external adopters or production users
|
|
30
|
+
yet**, and rung-3 external/community validation is openly tracked as not-yet-met.
|
|
31
|
+
The 4-tool surface is locked by a
|
|
32
|
+
[surface test](https://github.com/mbeacom/adrkit/blob/main/packages/mcp/test/surface.test.ts).
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
The published binary is **`adrkit-mcp`**. Run it with `npx` (no install):
|
|
14
37
|
|
|
15
38
|
```sh
|
|
16
|
-
|
|
17
|
-
|
|
39
|
+
npx -y @adrkit/mcp --cwd /path/to/your/repo --dir docs/adr
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
It speaks JSON-RPC over stdio, so you normally point an MCP client at it rather than
|
|
43
|
+
running it by hand. Copy-pasteable client configs follow.
|
|
44
|
+
|
|
45
|
+
### Protocol revisions
|
|
46
|
+
|
|
47
|
+
The server speaks **both** MCP protocol eras on the same stdio connection, and the
|
|
48
|
+
client picks. The opening exchange selects the era and pins it for the connection's
|
|
49
|
+
lifetime:
|
|
50
|
+
|
|
51
|
+
| Client opens with | Server serves |
|
|
52
|
+
| ----------------------------------------------------- | ------------------------------------------------- |
|
|
53
|
+
| `server/discover`, or any request carrying a 2026 `_meta` envelope | **`2026-07-28`** — stateless, no handshake |
|
|
54
|
+
| `initialize` / `notifications/initialized` | the 2025-era revision it negotiates |
|
|
55
|
+
|
|
56
|
+
On `2026-07-28` there is no `initialize` handshake and no session id: every request
|
|
57
|
+
carries its own protocol version and client capabilities in `_meta`, and every result
|
|
58
|
+
is self-describing (`resultType`, plus server identity in `_meta`). `tools/list` and
|
|
59
|
+
`server/discover` are cacheable (SEP-2549) and are served with `ttlMs: 300000,
|
|
60
|
+
cacheScope: "public"` — the four-tool surface is immutable for the life of the process
|
|
61
|
+
and carries no corpus content, so a client may reuse it instead of re-listing. Corpus
|
|
62
|
+
reads are never cacheable: every `tools/call` loads a fresh projection.
|
|
63
|
+
|
|
64
|
+
Nothing else about the tools changes between eras — same names, same schemas, same
|
|
65
|
+
annotations, same structured results. The server uses none of the features the
|
|
66
|
+
`2026-07-28` revision deprecated (roots, sampling, logging) or removed (sessions,
|
|
67
|
+
`ping`, `resources/subscribe`).
|
|
68
|
+
|
|
69
|
+
### Claude Desktop
|
|
70
|
+
|
|
71
|
+
Edit `claude_desktop_config.json` (macOS:
|
|
72
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`). Claude launches
|
|
73
|
+
servers from an arbitrary working directory, so set `ADRKIT_MCP_CWD` to your repo's
|
|
74
|
+
absolute path:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"adrkit": {
|
|
80
|
+
"command": "npx",
|
|
81
|
+
"args": ["-y", "@adrkit/mcp"],
|
|
82
|
+
"env": {
|
|
83
|
+
"ADRKIT_MCP_CWD": "/absolute/path/to/your/repo",
|
|
84
|
+
"ADRKIT_MCP_DIR": "docs/adr"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
18
89
|
```
|
|
19
90
|
|
|
91
|
+
### VS Code
|
|
92
|
+
|
|
93
|
+
Create `.vscode/mcp.json` in your workspace (VS Code uses the `servers` key and
|
|
94
|
+
substitutes `${workspaceFolder}`):
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"servers": {
|
|
99
|
+
"adrkit": {
|
|
100
|
+
"type": "stdio",
|
|
101
|
+
"command": "npx",
|
|
102
|
+
"args": ["-y", "@adrkit/mcp", "--cwd", "${workspaceFolder}", "--dir", "docs/adr"]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Cursor
|
|
109
|
+
|
|
110
|
+
Create `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"mcpServers": {
|
|
115
|
+
"adrkit": {
|
|
116
|
+
"command": "npx",
|
|
117
|
+
"args": ["-y", "@adrkit/mcp"],
|
|
118
|
+
"env": {
|
|
119
|
+
"ADRKIT_MCP_CWD": "/absolute/path/to/your/repo",
|
|
120
|
+
"ADRKIT_MCP_DIR": "docs/adr"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### GitHub Copilot CLI
|
|
128
|
+
|
|
129
|
+
Add to `~/.copilot/mcp-config.json` (user-level) or `./.copilot/mcp-config.json`
|
|
130
|
+
(per-repo). Copilot CLI runs servers from the trusted repo directory, so the default
|
|
131
|
+
`cwd` usually resolves correctly:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"mcpServers": {
|
|
136
|
+
"adrkit": {
|
|
137
|
+
"type": "local",
|
|
138
|
+
"command": "npx",
|
|
139
|
+
"args": ["-y", "@adrkit/mcp", "--dir", "docs/adr"],
|
|
140
|
+
"tools": ["*"]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
You can also add it interactively with `/mcp add` inside a `copilot` session.
|
|
147
|
+
|
|
148
|
+
### Configuration reference
|
|
149
|
+
|
|
20
150
|
| Option | Env | Default | Meaning |
|
|
21
151
|
|---|---|---|---|
|
|
22
152
|
| `--cwd <path>` | `ADRKIT_MCP_CWD` | `process.cwd()` | Repository root; must canonicalize to a directory containing a readable `.git` entry (a normal clone or a linked-worktree `.git` file). |
|
|
@@ -27,6 +157,148 @@ configuration exits non-zero with a diagnostic on **stderr** (`2` for an
|
|
|
27
157
|
unparseable flag, `1` for an invalid root/directory) and never starts a transport.
|
|
28
158
|
**stdout is reserved for JSON-RPC protocol frames only.**
|
|
29
159
|
|
|
160
|
+
## The four tools
|
|
161
|
+
|
|
162
|
+
Exactly four tools, all read-only. Each shares fixed annotations
|
|
163
|
+
(`readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`,
|
|
164
|
+
`openWorldHint: false`), returns a deterministic human-readable summary line in
|
|
165
|
+
`content[0].text`, and carries a `findings` page with the corpus's own
|
|
166
|
+
parse/validation findings. Every substantive response also includes a
|
|
167
|
+
`corpusHealth` sibling: `{ fingerprint, recordCount, excludedCount }`.
|
|
168
|
+
|
|
169
|
+
Shapes below are the real input/output contracts (see
|
|
170
|
+
[`packages/mcp/src/tools`](https://github.com/mbeacom/adrkit/tree/main/packages/mcp/src/tools)).
|
|
171
|
+
Each tool's structured output is `{ corpusHealth?, result }`; the objects shown
|
|
172
|
+
under "output" are the members of that discriminated `result` union (keyed on
|
|
173
|
+
`outcome`). `findings` and every growing array are cursor-paginated (see
|
|
174
|
+
[Pagination](#pagination-and-cursor-restart)); `findings` and repeated pagination
|
|
175
|
+
fields are elided here for brevity.
|
|
176
|
+
|
|
177
|
+
### `search_decisions`
|
|
178
|
+
|
|
179
|
+
Normalized literal substring search over id, title, tags, and body (the graveyard of
|
|
180
|
+
superseded/rejected records is included by default). Filters are ANDed:
|
|
181
|
+
`status`/`scope` match any-of; `tags` matches all-of.
|
|
182
|
+
|
|
183
|
+
```jsonc
|
|
184
|
+
// input
|
|
185
|
+
{
|
|
186
|
+
"query": "postgres", // required, 1–256 code units, non-empty after trim
|
|
187
|
+
"status": ["accepted"], // optional, ≤6, any-of
|
|
188
|
+
"tags": ["database"], // optional, ≤32 tags × ≤64 chars, all-of
|
|
189
|
+
"scope": ["backend"] // optional, ≤3, any-of
|
|
190
|
+
}
|
|
191
|
+
// output → single "results" branch (empty results use the same branch)
|
|
192
|
+
{
|
|
193
|
+
"outcome": "results",
|
|
194
|
+
"items": [
|
|
195
|
+
{
|
|
196
|
+
"id": "0007",
|
|
197
|
+
"title": "Adopt Postgres",
|
|
198
|
+
"status": "accepted",
|
|
199
|
+
"sourcePath": "docs/adr/0007-adopt-postgres.md",
|
|
200
|
+
"matchedFields": ["title", "body"] // subset of id | title | tag | body
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
"cursor": null
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `get_decision`
|
|
208
|
+
|
|
209
|
+
The complete typed frontmatter + body for one ref.
|
|
210
|
+
|
|
211
|
+
```jsonc
|
|
212
|
+
// input
|
|
213
|
+
{ "ref": "0007" } // AdrRef, 1–128 chars (local id, or a "log:id" federated ref)
|
|
214
|
+
// output → discriminated on "outcome"
|
|
215
|
+
// found (the full record is nested under "decision"):
|
|
216
|
+
{
|
|
217
|
+
"outcome": "found",
|
|
218
|
+
"decision": {
|
|
219
|
+
"requestedRef": "0007",
|
|
220
|
+
"id": "0007",
|
|
221
|
+
"title": "Adopt Postgres",
|
|
222
|
+
"status": "accepted",
|
|
223
|
+
"sourcePath": "docs/adr/0007-adopt-postgres.md",
|
|
224
|
+
"frontmatter": { /* typed YAML: status, tags, affects, relations, ... */ },
|
|
225
|
+
"body": "## Context\n..."
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// other outcomes:
|
|
229
|
+
// { "outcome": "not-found", "requestedRef": "9999" }
|
|
230
|
+
// { "outcome": "ambiguous-local-id", "requestedRef": "0007", "candidates": [ /* DecisionSummary[] */ ] }
|
|
231
|
+
// { "outcome": "federated-log-unavailable", "requestedRef": "core:12", "log": "core", "id": "12" }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
A `log:id` federated ref is recognized but **never resolved or substituted** — this
|
|
235
|
+
server reads exactly one local corpus.
|
|
236
|
+
|
|
237
|
+
### `get_decision_context`
|
|
238
|
+
|
|
239
|
+
Governing / active-proposal / historical decisions for repo-relative `files[]`, via
|
|
240
|
+
each record's own `affects` matchers. **Paths are compared against patterns only —
|
|
241
|
+
never opened.**
|
|
242
|
+
|
|
243
|
+
```jsonc
|
|
244
|
+
// input
|
|
245
|
+
{
|
|
246
|
+
"files": ["src/db/pool.ts", "src/db/schema.sql"]
|
|
247
|
+
// 1–256 entries; each POSIX, 1–1024 chars; no leading "/", no "..", no drive, no "\"
|
|
248
|
+
}
|
|
249
|
+
// output → single "matches" branch (all three arrays; empty is the same branch)
|
|
250
|
+
{
|
|
251
|
+
"outcome": "matches",
|
|
252
|
+
"governing": [
|
|
253
|
+
{
|
|
254
|
+
"id": "0007",
|
|
255
|
+
"title": "Adopt Postgres",
|
|
256
|
+
"status": "accepted",
|
|
257
|
+
"sourcePath": "docs/adr/0007-adopt-postgres.md",
|
|
258
|
+
"firedMatchers": [ { "type": "path", "pattern": "src/db/**" } ],
|
|
259
|
+
"relations": { "supersedes": [], "supersededBy": null, "relatesTo": [], "conflictsWith": [] }
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
"activeProposals": [],
|
|
263
|
+
"history": []
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `list_superseded`
|
|
268
|
+
|
|
269
|
+
Every superseded record with its **direct** local replacement state — the tool that
|
|
270
|
+
keeps agents from re-proposing rejected paths.
|
|
271
|
+
|
|
272
|
+
```jsonc
|
|
273
|
+
// input (pagination only)
|
|
274
|
+
{}
|
|
275
|
+
// output → single "entries" branch
|
|
276
|
+
{
|
|
277
|
+
"outcome": "entries",
|
|
278
|
+
"items": [
|
|
279
|
+
{
|
|
280
|
+
"id": "0003",
|
|
281
|
+
"title": "Use MySQL",
|
|
282
|
+
"status": "superseded",
|
|
283
|
+
"sourcePath": "docs/adr/0003-use-mysql.md",
|
|
284
|
+
"supersededBy": { // one of four states:
|
|
285
|
+
"resolved": true,
|
|
286
|
+
"target": { "id": "0007", "title": "Adopt Postgres", "status": "accepted", "sourcePath": "docs/adr/0007-adopt-postgres.md" }
|
|
287
|
+
}
|
|
288
|
+
// unresolved states:
|
|
289
|
+
// { "resolved": false, "targetRef": "0099", "reason": "dangling" }
|
|
290
|
+
// { "resolved": false, "targetRef": "0007", "reason": "ambiguous", "candidateCount": 2 }
|
|
291
|
+
// { "resolved": false, "targetRef": "core:1", "reason": "federated-unavailable", "log": "core", "id": "1" }
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
"cursor": null
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Relation refs (`supersedes`, `supersededBy`, `relatesTo`, `conflictsWith`) are
|
|
299
|
+
surfaced verbatim and never expanded — follow them with a second `get_decision`
|
|
300
|
+
call. Supersession is reported one hop deep; there is no transitive traversal.
|
|
301
|
+
|
|
30
302
|
## Library surface
|
|
31
303
|
|
|
32
304
|
The package root exports only a sealed lifecycle factory. There is no way to reach
|
|
@@ -36,7 +308,7 @@ the underlying SDK server, its registrations, or its transport:
|
|
|
36
308
|
import { createAdrkitMcpServer } from '@adrkit/mcp';
|
|
37
309
|
|
|
38
310
|
const server = createAdrkitMcpServer({ cwd: process.cwd(), dir: 'docs/adr' });
|
|
39
|
-
await server.start(); // validates the root,
|
|
311
|
+
await server.start(); // validates the root, then serves one stdio connection
|
|
40
312
|
// ... later:
|
|
41
313
|
await server.close();
|
|
42
314
|
```
|
|
@@ -44,23 +316,11 @@ await server.close();
|
|
|
44
316
|
`createAdrkitMcpServer(options?)` performs no filesystem access at construction and
|
|
45
317
|
returns a frozen, null-prototype handle with exactly `start()` and `close()`.
|
|
46
318
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
response, and a `findings` page carrying the corpus's own parse/validation findings.
|
|
53
|
-
|
|
54
|
-
| Tool | Answers | Notable outcomes |
|
|
55
|
-
|---|---|---|
|
|
56
|
-
| `search_decisions` | Normalized literal substring search over id, title, tags, and body (graveyard included by default). Filters: `status`/`scope` (any-of), `tags` (all-of), ANDed. | `results` (empty is the same branch) |
|
|
57
|
-
| `get_decision` | The complete typed frontmatter + body for one ref. | `found`, `not-found`, `ambiguous-local-id` (duplicate ids), `federated-log-unavailable` (a `log:id` ref is recognized, never resolved or substituted) |
|
|
58
|
-
| `get_decision_context` | Governing / active-proposal / historical decisions for repo-relative `files[]`, via the corpus's own `affects` matchers. Paths are compared against patterns only — never opened. | `matches` (all three arrays; empty is the same branch) |
|
|
59
|
-
| `list_superseded` | Every superseded record with its **direct** local replacement state. | `entries` with `resolved` / `dangling` / `ambiguous` (`candidateCount` only) / `federated-unavailable` targets |
|
|
60
|
-
|
|
61
|
-
Relation refs (`supersedes`, `supersededBy`, `relatesTo`, `conflictsWith`) are
|
|
62
|
-
surfaced verbatim and never expanded — follow them with a second `get_decision`
|
|
63
|
-
call.
|
|
319
|
+
Transport failures — a transport that fails to start, or a background stream error
|
|
320
|
+
such as the `EPIPE` from a client that has gone away — are reported through the
|
|
321
|
+
optional `onError` option, which defaults to a stderr diagnostic. The `adrkit-mcp`
|
|
322
|
+
binary additionally exits non-zero. Nothing is written to stdout: that is reserved
|
|
323
|
+
for protocol frames.
|
|
64
324
|
|
|
65
325
|
## Limits
|
|
66
326
|
|
|
@@ -80,7 +340,8 @@ an unchanged corpus; otherwise the response is a non-error `invalid-cursor` outc
|
|
|
80
340
|
(`corpus-changed`, `query-mismatch`, `wrong-channel`, `offset-out-of-range`,
|
|
81
341
|
`cursor-not-applicable`, `version-unsupported`, or `decode-failed`) and you should
|
|
82
342
|
restart the walk from no cursor. The primary-result and `findings` channels page
|
|
83
|
-
independently.
|
|
343
|
+
independently. A `corpus-unavailable` outcome is returned when the ADR directory
|
|
344
|
+
cannot be read.
|
|
84
345
|
|
|
85
346
|
## Boundaries (out of scope by design)
|
|
86
347
|
|
|
@@ -94,6 +355,6 @@ traversal.
|
|
|
94
355
|
|
|
95
356
|
Developed and tested with Bun; published artifacts are ESM targeting Node.js `>=22`
|
|
96
357
|
and are verified on Node 22 and 24. Runtime dependencies are exactly `@adrkit/core`,
|
|
97
|
-
`@modelcontextprotocol/
|
|
358
|
+
`@modelcontextprotocol/server` (MCP TypeScript SDK v2), and `zod`.
|
|
98
359
|
|
|
99
360
|
Apache-2.0.
|
package/dist/bin.js
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
4
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
5
5
|
import { resolve as resolve2 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/server.ts
|
|
8
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
9
9
|
|
|
10
10
|
// src/corpus/ordering.ts
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return [...findings].sort(compareFindings);
|
|
19
|
-
}
|
|
11
|
+
import {
|
|
12
|
+
compareByIdThenPath,
|
|
13
|
+
compareCodeUnits,
|
|
14
|
+
compareFindings,
|
|
15
|
+
sortByIdThenPath,
|
|
16
|
+
sortFindingsCanonical
|
|
17
|
+
} from "@adrkit/core";
|
|
20
18
|
|
|
21
19
|
// src/search/normalize.ts
|
|
22
20
|
function normalize(value) {
|
|
@@ -107,9 +105,13 @@ import { AdrFrontmatter, AdrRef, Status, Scope } from "@adrkit/core";
|
|
|
107
105
|
|
|
108
106
|
// src/corpus/projection.ts
|
|
109
107
|
import { access, constants as FS, lstat, realpath, stat } from "node:fs/promises";
|
|
110
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
111
108
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
112
|
-
import {
|
|
109
|
+
import {
|
|
110
|
+
discoverAdrFiles,
|
|
111
|
+
fingerprintOf,
|
|
112
|
+
lintCorpus,
|
|
113
|
+
normalizeDisplayPath
|
|
114
|
+
} from "@adrkit/core";
|
|
113
115
|
var MAX_SOURCE_BYTES = 64 * 1024;
|
|
114
116
|
|
|
115
117
|
class CorpusUnavailableError extends Error {
|
|
@@ -207,29 +209,6 @@ async function verifyRoots(options) {
|
|
|
207
209
|
fail("root-not-found");
|
|
208
210
|
return roots;
|
|
209
211
|
}
|
|
210
|
-
function canonicalStringify(value) {
|
|
211
|
-
if (value === null || value === undefined)
|
|
212
|
-
return "null";
|
|
213
|
-
if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
|
|
214
|
-
return JSON.stringify(value);
|
|
215
|
-
}
|
|
216
|
-
if (Array.isArray(value))
|
|
217
|
-
return `[${value.map(canonicalStringify).join(",")}]`;
|
|
218
|
-
if (typeof value === "object") {
|
|
219
|
-
const record = value;
|
|
220
|
-
const keys = Object.keys(record).filter((key) => record[key] !== undefined).sort(compareCodeUnits);
|
|
221
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalStringify(record[key])}`).join(",")}}`;
|
|
222
|
-
}
|
|
223
|
-
return "null";
|
|
224
|
-
}
|
|
225
|
-
function fingerprintOf(records, corpusFindings, recordCount, excludedCount) {
|
|
226
|
-
const projection = {
|
|
227
|
-
records: records.map((record) => ({ sourcePath: record.path, frontmatter: record.frontmatter, body: record.body })),
|
|
228
|
-
corpusFindings,
|
|
229
|
-
corpusHealth: { recordCount, excludedCount }
|
|
230
|
-
};
|
|
231
|
-
return createHash2("sha256").update(canonicalStringify(projection), "utf8").digest("hex");
|
|
232
|
-
}
|
|
233
212
|
async function loadCorpusProjection(options) {
|
|
234
213
|
const roots = await verifyRoots(options);
|
|
235
214
|
let candidates;
|
|
@@ -501,7 +480,7 @@ function searchDecisionsOutputSchema() {
|
|
|
501
480
|
sourcePath: z.string(),
|
|
502
481
|
matchedFields: z.array(z.enum(["id", "title", "tag", "body"]))
|
|
503
482
|
});
|
|
504
|
-
return {
|
|
483
|
+
return z.object({
|
|
505
484
|
corpusHealth: corpusHealthSchema().optional(),
|
|
506
485
|
result: z.discriminatedUnion("outcome", [
|
|
507
486
|
z.object({
|
|
@@ -513,7 +492,7 @@ function searchDecisionsOutputSchema() {
|
|
|
513
492
|
invalidCursorSchema(),
|
|
514
493
|
corpusUnavailableSchema()
|
|
515
494
|
])
|
|
516
|
-
};
|
|
495
|
+
});
|
|
517
496
|
}
|
|
518
497
|
function getDecisionOutputSchema() {
|
|
519
498
|
const fullDecision = z.object({
|
|
@@ -525,7 +504,7 @@ function getDecisionOutputSchema() {
|
|
|
525
504
|
frontmatter: AdrFrontmatter,
|
|
526
505
|
body: z.string()
|
|
527
506
|
});
|
|
528
|
-
return {
|
|
507
|
+
return z.object({
|
|
529
508
|
corpusHealth: corpusHealthSchema().optional(),
|
|
530
509
|
result: z.discriminatedUnion("outcome", [
|
|
531
510
|
z.object({ outcome: z.literal("found"), decision: fullDecision, findings: findingsPageSchema() }),
|
|
@@ -547,7 +526,7 @@ function getDecisionOutputSchema() {
|
|
|
547
526
|
invalidCursorSchema(),
|
|
548
527
|
corpusUnavailableSchema()
|
|
549
528
|
])
|
|
550
|
-
};
|
|
529
|
+
});
|
|
551
530
|
}
|
|
552
531
|
function getDecisionContextOutputSchema() {
|
|
553
532
|
const contextEntry = z.object({
|
|
@@ -558,7 +537,7 @@ function getDecisionContextOutputSchema() {
|
|
|
558
537
|
firedMatchers: z.array(z.object({ type: z.string(), pattern: z.string() })),
|
|
559
538
|
relations: relationRefsSchema()
|
|
560
539
|
});
|
|
561
|
-
return {
|
|
540
|
+
return z.object({
|
|
562
541
|
corpusHealth: corpusHealthSchema().optional(),
|
|
563
542
|
result: z.discriminatedUnion("outcome", [
|
|
564
543
|
z.object({
|
|
@@ -572,7 +551,7 @@ function getDecisionContextOutputSchema() {
|
|
|
572
551
|
invalidCursorSchema(),
|
|
573
552
|
corpusUnavailableSchema()
|
|
574
553
|
])
|
|
575
|
-
};
|
|
554
|
+
});
|
|
576
555
|
}
|
|
577
556
|
function listSupersededOutputSchema() {
|
|
578
557
|
const supersededBy = z.union([
|
|
@@ -599,7 +578,7 @@ function listSupersededOutputSchema() {
|
|
|
599
578
|
sourcePath: z.string(),
|
|
600
579
|
supersededBy
|
|
601
580
|
});
|
|
602
|
-
return {
|
|
581
|
+
return z.object({
|
|
603
582
|
corpusHealth: corpusHealthSchema().optional(),
|
|
604
583
|
result: z.discriminatedUnion("outcome", [
|
|
605
584
|
z.object({
|
|
@@ -611,7 +590,7 @@ function listSupersededOutputSchema() {
|
|
|
611
590
|
invalidCursorSchema(),
|
|
612
591
|
corpusUnavailableSchema()
|
|
613
592
|
])
|
|
614
|
-
};
|
|
593
|
+
});
|
|
615
594
|
}
|
|
616
595
|
function structuredResult(result, text, corpusHealth) {
|
|
617
596
|
const structuredContent = corpusHealth === undefined ? { result } : { corpusHealth, result };
|
|
@@ -845,13 +824,9 @@ function registerGetDecision(server, config) {
|
|
|
845
824
|
}
|
|
846
825
|
|
|
847
826
|
// src/tools/get-decision-context.ts
|
|
848
|
-
import { resolveAffects } from "@adrkit/core";
|
|
827
|
+
import { decisionBucketFor, resolveAffects } from "@adrkit/core";
|
|
849
828
|
function bucketFor(status) {
|
|
850
|
-
|
|
851
|
-
return "governing";
|
|
852
|
-
if (status === "draft" || status === "proposed")
|
|
853
|
-
return "activeProposals";
|
|
854
|
-
return "history";
|
|
829
|
+
return decisionBucketFor(status);
|
|
855
830
|
}
|
|
856
831
|
function contextEntry(record, firedMatchers) {
|
|
857
832
|
return { ...toSummary(record), firedMatchers, relations: toRelationRefs(record.frontmatter) };
|
|
@@ -1018,21 +993,30 @@ function registerListSuperseded(server, config) {
|
|
|
1018
993
|
}
|
|
1019
994
|
|
|
1020
995
|
// src/server.ts
|
|
1021
|
-
var SERVER_INFO = { name: "@adrkit/mcp", version: "0.
|
|
996
|
+
var SERVER_INFO = { name: "@adrkit/mcp", version: "0.3.0" };
|
|
997
|
+
var CACHE_HINTS = {
|
|
998
|
+
"tools/list": { ttlMs: 300000, cacheScope: "public" },
|
|
999
|
+
"server/discover": { ttlMs: 300000, cacheScope: "public" }
|
|
1000
|
+
};
|
|
1022
1001
|
function buildRegisteredServer(config) {
|
|
1023
|
-
const server = new McpServer(SERVER_INFO);
|
|
1024
|
-
registerSearchDecisions(server, config);
|
|
1002
|
+
const server = new McpServer(SERVER_INFO, { cacheHints: CACHE_HINTS });
|
|
1025
1003
|
registerGetDecision(server, config);
|
|
1026
1004
|
registerGetDecisionContext(server, config);
|
|
1027
1005
|
registerListSuperseded(server, config);
|
|
1006
|
+
registerSearchDecisions(server, config);
|
|
1028
1007
|
return server;
|
|
1029
1008
|
}
|
|
1030
1009
|
|
|
1031
1010
|
// src/index.ts
|
|
1011
|
+
function writeTransportDiagnostic(error) {
|
|
1012
|
+
process.stderr.write(`adrkit-mcp: transport error: ${error.message}
|
|
1013
|
+
`);
|
|
1014
|
+
}
|
|
1032
1015
|
function createAdrkitMcpServer(options) {
|
|
1033
1016
|
const cwd = resolve2(options?.cwd ?? process.cwd());
|
|
1034
1017
|
const dir = options?.dir ?? "docs/adr";
|
|
1035
|
-
|
|
1018
|
+
const onError = options?.onError ?? writeTransportDiagnostic;
|
|
1019
|
+
let connection;
|
|
1036
1020
|
let startPromise;
|
|
1037
1021
|
let closePromise;
|
|
1038
1022
|
let closed = false;
|
|
@@ -1043,7 +1027,6 @@ function createAdrkitMcpServer(options) {
|
|
|
1043
1027
|
if (startPromise)
|
|
1044
1028
|
return startPromise;
|
|
1045
1029
|
startPromise = (async () => {
|
|
1046
|
-
let nextServer;
|
|
1047
1030
|
try {
|
|
1048
1031
|
const roots = await resolveCanonicalRoots({ cwd, dir });
|
|
1049
1032
|
const config = {
|
|
@@ -1052,19 +1035,9 @@ function createAdrkitMcpServer(options) {
|
|
|
1052
1035
|
expectedCanonicalCwd: roots.canonicalCwd,
|
|
1053
1036
|
maxSourceBytes: MAX_SOURCE_BYTES
|
|
1054
1037
|
};
|
|
1055
|
-
|
|
1056
|
-
server = nextServer;
|
|
1057
|
-
await nextServer.connect(new StdioServerTransport);
|
|
1038
|
+
connection = serveStdio(() => buildRegisteredServer(config), { onerror: onError });
|
|
1058
1039
|
} catch (error) {
|
|
1059
|
-
|
|
1060
|
-
if (nextServer) {
|
|
1061
|
-
try {
|
|
1062
|
-
await nextServer.close();
|
|
1063
|
-
} catch (closeError) {
|
|
1064
|
-
startPromise = undefined;
|
|
1065
|
-
throw new AggregateError([error, closeError], "MCP server startup and cleanup failed");
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1040
|
+
connection = undefined;
|
|
1068
1041
|
startPromise = undefined;
|
|
1069
1042
|
throw error;
|
|
1070
1043
|
}
|
|
@@ -1078,8 +1051,8 @@ function createAdrkitMcpServer(options) {
|
|
|
1078
1051
|
closePromise = (async () => {
|
|
1079
1052
|
if (startPromise)
|
|
1080
1053
|
await startPromise;
|
|
1081
|
-
const current =
|
|
1082
|
-
|
|
1054
|
+
const current = connection;
|
|
1055
|
+
connection = undefined;
|
|
1083
1056
|
if (current)
|
|
1084
1057
|
await current.close();
|
|
1085
1058
|
})();
|
|
@@ -1113,6 +1086,13 @@ function reportUnhandledRejection(reason, write = writeStderr, fail2 = () => {
|
|
|
1113
1086
|
`);
|
|
1114
1087
|
fail2();
|
|
1115
1088
|
}
|
|
1089
|
+
function reportTransportError(error, write = writeStderr, fail2 = () => {
|
|
1090
|
+
process.exitCode = 1;
|
|
1091
|
+
}) {
|
|
1092
|
+
write(`adrkit-mcp: transport error: ${error.message}
|
|
1093
|
+
`);
|
|
1094
|
+
fail2();
|
|
1095
|
+
}
|
|
1116
1096
|
async function main(argv, env) {
|
|
1117
1097
|
let values;
|
|
1118
1098
|
try {
|
|
@@ -1139,7 +1119,7 @@ ${USAGE}`);
|
|
|
1139
1119
|
}
|
|
1140
1120
|
throw error;
|
|
1141
1121
|
}
|
|
1142
|
-
const handle = createAdrkitMcpServer({ cwd, dir });
|
|
1122
|
+
const handle = createAdrkitMcpServer({ cwd, dir, onError: (error) => reportTransportError(error) });
|
|
1143
1123
|
const shutdown = () => {
|
|
1144
1124
|
handle.close().finally(() => process.exit(0));
|
|
1145
1125
|
};
|
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @adrkit/mcp —
|
|
3
|
-
*
|
|
2
|
+
* @adrkit/mcp — re-export shim.
|
|
3
|
+
*
|
|
4
|
+
* The comparator and canonical orderings were promoted to `@adrkit/core`
|
|
5
|
+
* (`packages/core/src/ordering/index.ts`). This module preserves every existing
|
|
6
|
+
* `../corpus/ordering` import site while the single implementation now lives in core.
|
|
4
7
|
*/
|
|
5
|
-
|
|
6
|
-
/** The sole code-unit comparator: `a < b ? -1 : a > b ? 1 : 0` over UTF-16 units. */
|
|
7
|
-
export declare function compareCodeUnits(a: string, b: string): number;
|
|
8
|
-
export interface OrderedSummary {
|
|
9
|
-
readonly id: string;
|
|
10
|
-
readonly sourcePath: string;
|
|
11
|
-
}
|
|
12
|
-
/** Canonical `(id, sourcePath)` ascending order; sourcePath is the unique tiebreak. */
|
|
13
|
-
export declare function compareByIdThenPath(a: OrderedSummary, b: OrderedSummary): number;
|
|
14
|
-
/** Canonical finding order using `sortFindings`' field tuple with the code-unit comparator. */
|
|
15
|
-
export declare function compareFindings(a: Finding, b: Finding): number;
|
|
16
|
-
export declare function sortFindingsCanonical(findings: readonly Finding[]): Finding[];
|
|
17
|
-
export declare function sortByIdThenPath<T extends OrderedSummary>(items: readonly T[]): T[];
|
|
8
|
+
export { compareByIdThenPath, compareCodeUnits, compareFindings, sortByIdThenPath, sortFindingsCanonical, type OrderedSummary, } from '@adrkit/core';
|