@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 CHANGED
@@ -1,22 +1,152 @@
1
1
  # `@adrkit/mcp`
2
2
 
3
- A **local, read-only** [Model Context Protocol](https://modelcontextprotocol.io)
4
- server that exposes adrkit decision retrieval over stdio. It lets an agent harness
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
- > Part of [adrkit](https://adrkit.dev). The corpus lives in git as one Markdown
10
- > file per decision with typed YAML frontmatter (`@adrkit/core`); this server only
11
- > reads it.
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
- ## Install and run
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
- bunx @adrkit/mcp # run the adrkit-mcp bin
17
- adrkit-mcp --cwd /path/to/repo --dir docs/adr
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, connects exactly one stdio transport
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
- ## The four tools
48
-
49
- All four share fixed annotations (`readOnlyHint: true`, `destructiveHint: false`,
50
- `idempotentHint: true`, `openWorldHint: false`), a `corpusHealth`
51
- (`fingerprint`/`recordCount`/`excludedCount`) sibling on every substantive
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/sdk`, and `zod`.
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 { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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/sdk/server/mcp.js";
8
+ import { McpServer } from "@modelcontextprotocol/server";
9
9
 
10
10
  // src/corpus/ordering.ts
11
- function compareCodeUnits(a, b) {
12
- return a < b ? -1 : a > b ? 1 : 0;
13
- }
14
- function compareFindings(a, b) {
15
- return compareCodeUnits(a.rule, b.rule) || compareCodeUnits(a.id ?? "", b.id ?? "") || compareCodeUnits(a.pattern ?? "", b.pattern ?? "") || compareCodeUnits(a.path ?? "", b.path ?? "") || compareCodeUnits(a.field ?? "", b.field ?? "") || compareCodeUnits(a.message, b.message);
16
- }
17
- function sortFindingsCanonical(findings) {
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 { discoverAdrFiles, lintCorpus, normalizeDisplayPath } from "@adrkit/core";
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
- if (status === "accepted")
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.1.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
- let server;
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
- nextServer = buildRegisteredServer(config);
1056
- server = nextServer;
1057
- await nextServer.connect(new StdioServerTransport);
1038
+ connection = serveStdio(() => buildRegisteredServer(config), { onerror: onError });
1058
1039
  } catch (error) {
1059
- server = undefined;
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 = server;
1082
- server = undefined;
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 — the one locale-independent comparator and the canonical orderings
3
- * every channel uses. Never `String.prototype.localeCompare` (research §R6).
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
- import type { Finding } from '@adrkit/core';
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';