@adrkit/mcp 0.2.0 → 0.2.1

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,126 @@
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. Registry namespace: **`dev.adrkit/mcp`** (this is the Node/npm `@adrkit/mcp`
20
+ package — unrelated to the `adr-kit` Python package on PyPI).
21
+
22
+ ## Maturity
23
+
24
+ adrkit is **early**. Phases 0–6 are *landed / reference-verified* against
25
+ [ADR-0014](https://github.com/mbeacom/adrkit/blob/main/docs/adr/0014-stage-phase-landing-evidence-across-a-three-rung-validation-ladder.md)
26
+ rungs 1–2 (unit/contract/conformance plus maintainer-owned isolated
27
+ reference-repository validation). It has **no external adopters or production users
28
+ yet**, and rung-3 external/community validation is openly tracked as not-yet-met.
29
+ The 4-tool surface is locked by a
30
+ [surface test](https://github.com/mbeacom/adrkit/blob/main/packages/mcp/test/surface.test.ts).
31
+
32
+ ## Quick start
33
+
34
+ The published binary is **`adrkit-mcp`**. Run it with `npx` (no install):
14
35
 
15
36
  ```sh
16
- bunx @adrkit/mcp # run the adrkit-mcp bin
17
- adrkit-mcp --cwd /path/to/repo --dir docs/adr
37
+ npx -y @adrkit/mcp --cwd /path/to/your/repo --dir docs/adr
38
+ ```
39
+
40
+ It speaks JSON-RPC over stdio, so you normally point an MCP client at it rather than
41
+ running it by hand. Copy-pasteable client configs follow.
42
+
43
+ ### Claude Desktop
44
+
45
+ Edit `claude_desktop_config.json` (macOS:
46
+ `~/Library/Application Support/Claude/claude_desktop_config.json`). Claude launches
47
+ servers from an arbitrary working directory, so set `ADRKIT_MCP_CWD` to your repo's
48
+ absolute path:
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "adrkit": {
54
+ "command": "npx",
55
+ "args": ["-y", "@adrkit/mcp"],
56
+ "env": {
57
+ "ADRKIT_MCP_CWD": "/absolute/path/to/your/repo",
58
+ "ADRKIT_MCP_DIR": "docs/adr"
59
+ }
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ ### VS Code
66
+
67
+ Create `.vscode/mcp.json` in your workspace (VS Code uses the `servers` key and
68
+ substitutes `${workspaceFolder}`):
69
+
70
+ ```json
71
+ {
72
+ "servers": {
73
+ "adrkit": {
74
+ "type": "stdio",
75
+ "command": "npx",
76
+ "args": ["-y", "@adrkit/mcp", "--cwd", "${workspaceFolder}", "--dir", "docs/adr"]
77
+ }
78
+ }
79
+ }
18
80
  ```
19
81
 
82
+ ### Cursor
83
+
84
+ Create `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "adrkit": {
90
+ "command": "npx",
91
+ "args": ["-y", "@adrkit/mcp"],
92
+ "env": {
93
+ "ADRKIT_MCP_CWD": "/absolute/path/to/your/repo",
94
+ "ADRKIT_MCP_DIR": "docs/adr"
95
+ }
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### GitHub Copilot CLI
102
+
103
+ Add to `~/.copilot/mcp-config.json` (user-level) or `./.copilot/mcp-config.json`
104
+ (per-repo). Copilot CLI runs servers from the trusted repo directory, so the default
105
+ `cwd` usually resolves correctly:
106
+
107
+ ```json
108
+ {
109
+ "mcpServers": {
110
+ "adrkit": {
111
+ "type": "local",
112
+ "command": "npx",
113
+ "args": ["-y", "@adrkit/mcp", "--dir", "docs/adr"],
114
+ "tools": ["*"]
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ You can also add it interactively with `/mcp add` inside a `copilot` session.
121
+
122
+ ### Configuration reference
123
+
20
124
  | Option | Env | Default | Meaning |
21
125
  |---|---|---|---|
22
126
  | `--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 +131,148 @@ configuration exits non-zero with a diagnostic on **stderr** (`2` for an
27
131
  unparseable flag, `1` for an invalid root/directory) and never starts a transport.
28
132
  **stdout is reserved for JSON-RPC protocol frames only.**
29
133
 
134
+ ## The four tools
135
+
136
+ Exactly four tools, all read-only. Each shares fixed annotations
137
+ (`readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`,
138
+ `openWorldHint: false`), returns a deterministic human-readable summary line in
139
+ `content[0].text`, and carries a `findings` page with the corpus's own
140
+ parse/validation findings. Every substantive response also includes a
141
+ `corpusHealth` sibling: `{ fingerprint, recordCount, excludedCount }`.
142
+
143
+ Shapes below are the real input/output contracts (see
144
+ [`packages/mcp/src/tools`](https://github.com/mbeacom/adrkit/tree/main/packages/mcp/src/tools)).
145
+ Each tool's structured output is `{ corpusHealth?, result }`; the objects shown
146
+ under "output" are the members of that discriminated `result` union (keyed on
147
+ `outcome`). `findings` and every growing array are cursor-paginated (see
148
+ [Pagination](#pagination-and-cursor-restart)); `findings` and repeated pagination
149
+ fields are elided here for brevity.
150
+
151
+ ### `search_decisions`
152
+
153
+ Normalized literal substring search over id, title, tags, and body (the graveyard of
154
+ superseded/rejected records is included by default). Filters are ANDed:
155
+ `status`/`scope` match any-of; `tags` matches all-of.
156
+
157
+ ```jsonc
158
+ // input
159
+ {
160
+ "query": "postgres", // required, 1–256 code units, non-empty after trim
161
+ "status": ["accepted"], // optional, ≤6, any-of
162
+ "tags": ["database"], // optional, ≤32 tags × ≤64 chars, all-of
163
+ "scope": ["backend"] // optional, ≤3, any-of
164
+ }
165
+ // output → single "results" branch (empty results use the same branch)
166
+ {
167
+ "outcome": "results",
168
+ "items": [
169
+ {
170
+ "id": "0007",
171
+ "title": "Adopt Postgres",
172
+ "status": "accepted",
173
+ "sourcePath": "docs/adr/0007-adopt-postgres.md",
174
+ "matchedFields": ["title", "body"] // subset of id | title | tag | body
175
+ }
176
+ ],
177
+ "cursor": null
178
+ }
179
+ ```
180
+
181
+ ### `get_decision`
182
+
183
+ The complete typed frontmatter + body for one ref.
184
+
185
+ ```jsonc
186
+ // input
187
+ { "ref": "0007" } // AdrRef, 1–128 chars (local id, or a "log:id" federated ref)
188
+ // output → discriminated on "outcome"
189
+ // found (the full record is nested under "decision"):
190
+ {
191
+ "outcome": "found",
192
+ "decision": {
193
+ "requestedRef": "0007",
194
+ "id": "0007",
195
+ "title": "Adopt Postgres",
196
+ "status": "accepted",
197
+ "sourcePath": "docs/adr/0007-adopt-postgres.md",
198
+ "frontmatter": { /* typed YAML: status, tags, affects, relations, ... */ },
199
+ "body": "## Context\n..."
200
+ }
201
+ }
202
+ // other outcomes:
203
+ // { "outcome": "not-found", "requestedRef": "9999" }
204
+ // { "outcome": "ambiguous-local-id", "requestedRef": "0007", "candidates": [ /* DecisionSummary[] */ ] }
205
+ // { "outcome": "federated-log-unavailable", "requestedRef": "core:12", "log": "core", "id": "12" }
206
+ ```
207
+
208
+ A `log:id` federated ref is recognized but **never resolved or substituted** — this
209
+ server reads exactly one local corpus.
210
+
211
+ ### `get_decision_context`
212
+
213
+ Governing / active-proposal / historical decisions for repo-relative `files[]`, via
214
+ each record's own `affects` matchers. **Paths are compared against patterns only —
215
+ never opened.**
216
+
217
+ ```jsonc
218
+ // input
219
+ {
220
+ "files": ["src/db/pool.ts", "src/db/schema.sql"]
221
+ // 1–256 entries; each POSIX, 1–1024 chars; no leading "/", no "..", no drive, no "\"
222
+ }
223
+ // output → single "matches" branch (all three arrays; empty is the same branch)
224
+ {
225
+ "outcome": "matches",
226
+ "governing": [
227
+ {
228
+ "id": "0007",
229
+ "title": "Adopt Postgres",
230
+ "status": "accepted",
231
+ "sourcePath": "docs/adr/0007-adopt-postgres.md",
232
+ "firedMatchers": [ { "type": "path", "pattern": "src/db/**" } ],
233
+ "relations": { "supersedes": [], "supersededBy": null, "relatesTo": [], "conflictsWith": [] }
234
+ }
235
+ ],
236
+ "activeProposals": [],
237
+ "history": []
238
+ }
239
+ ```
240
+
241
+ ### `list_superseded`
242
+
243
+ Every superseded record with its **direct** local replacement state — the tool that
244
+ keeps agents from re-proposing rejected paths.
245
+
246
+ ```jsonc
247
+ // input (pagination only)
248
+ {}
249
+ // output → single "entries" branch
250
+ {
251
+ "outcome": "entries",
252
+ "items": [
253
+ {
254
+ "id": "0003",
255
+ "title": "Use MySQL",
256
+ "status": "superseded",
257
+ "sourcePath": "docs/adr/0003-use-mysql.md",
258
+ "supersededBy": { // one of four states:
259
+ "resolved": true,
260
+ "target": { "id": "0007", "title": "Adopt Postgres", "status": "accepted", "sourcePath": "docs/adr/0007-adopt-postgres.md" }
261
+ }
262
+ // unresolved states:
263
+ // { "resolved": false, "targetRef": "0099", "reason": "dangling" }
264
+ // { "resolved": false, "targetRef": "0007", "reason": "ambiguous", "candidateCount": 2 }
265
+ // { "resolved": false, "targetRef": "core:1", "reason": "federated-unavailable", "log": "core", "id": "1" }
266
+ }
267
+ ],
268
+ "cursor": null
269
+ }
270
+ ```
271
+
272
+ Relation refs (`supersedes`, `supersededBy`, `relatesTo`, `conflictsWith`) are
273
+ surfaced verbatim and never expanded — follow them with a second `get_decision`
274
+ call. Supersession is reported one hop deep; there is no transitive traversal.
275
+
30
276
  ## Library surface
31
277
 
32
278
  The package root exports only a sealed lifecycle factory. There is no way to reach
@@ -44,24 +290,6 @@ await server.close();
44
290
  `createAdrkitMcpServer(options?)` performs no filesystem access at construction and
45
291
  returns a frozen, null-prototype handle with exactly `start()` and `close()`.
46
292
 
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.
64
-
65
293
  ## Limits
66
294
 
67
295
  `query` 1–256 code units (non-empty after trimming); `ref` 1–128; `files[]` 1–256
@@ -80,7 +308,8 @@ an unchanged corpus; otherwise the response is a non-error `invalid-cursor` outc
80
308
  (`corpus-changed`, `query-mismatch`, `wrong-channel`, `offset-out-of-range`,
81
309
  `cursor-not-applicable`, `version-unsupported`, or `decode-failed`) and you should
82
310
  restart the walk from no cursor. The primary-result and `findings` channels page
83
- independently.
311
+ independently. A `corpus-unavailable` outcome is returned when the ADR directory
312
+ cannot be read.
84
313
 
85
314
  ## Boundaries (out of scope by design)
86
315
 
package/dist/bin.js CHANGED
@@ -8,15 +8,13 @@ import { resolve as resolve2 } from "node:path";
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
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;
@@ -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,7 +993,7 @@ 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.2.1" };
1022
997
  function buildRegisteredServer(config) {
1023
998
  const server = new McpServer(SERVER_INFO);
1024
999
  registerSearchDecisions(server, config);
@@ -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';
package/dist/index.js CHANGED
@@ -6,15 +6,13 @@ import { resolve as resolve2 } from "node:path";
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
 
8
8
  // src/corpus/ordering.ts
9
- function compareCodeUnits(a, b) {
10
- return a < b ? -1 : a > b ? 1 : 0;
11
- }
12
- function compareFindings(a, b) {
13
- 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);
14
- }
15
- function sortFindingsCanonical(findings) {
16
- return [...findings].sort(compareFindings);
17
- }
9
+ import {
10
+ compareByIdThenPath,
11
+ compareCodeUnits,
12
+ compareFindings,
13
+ sortByIdThenPath,
14
+ sortFindingsCanonical
15
+ } from "@adrkit/core";
18
16
 
19
17
  // src/search/normalize.ts
20
18
  function normalize(value) {
@@ -105,9 +103,13 @@ import { AdrFrontmatter, AdrRef, Status, Scope } from "@adrkit/core";
105
103
 
106
104
  // src/corpus/projection.ts
107
105
  import { access, constants as FS, lstat, realpath, stat } from "node:fs/promises";
108
- import { createHash as createHash2 } from "node:crypto";
109
106
  import { isAbsolute, relative, resolve, sep } from "node:path";
110
- import { discoverAdrFiles, lintCorpus, normalizeDisplayPath } from "@adrkit/core";
107
+ import {
108
+ discoverAdrFiles,
109
+ fingerprintOf,
110
+ lintCorpus,
111
+ normalizeDisplayPath
112
+ } from "@adrkit/core";
111
113
  var MAX_SOURCE_BYTES = 64 * 1024;
112
114
 
113
115
  class CorpusUnavailableError extends Error {
@@ -205,29 +207,6 @@ async function verifyRoots(options) {
205
207
  fail("root-not-found");
206
208
  return roots;
207
209
  }
208
- function canonicalStringify(value) {
209
- if (value === null || value === undefined)
210
- return "null";
211
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
212
- return JSON.stringify(value);
213
- }
214
- if (Array.isArray(value))
215
- return `[${value.map(canonicalStringify).join(",")}]`;
216
- if (typeof value === "object") {
217
- const record = value;
218
- const keys = Object.keys(record).filter((key) => record[key] !== undefined).sort(compareCodeUnits);
219
- return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalStringify(record[key])}`).join(",")}}`;
220
- }
221
- return "null";
222
- }
223
- function fingerprintOf(records, corpusFindings, recordCount, excludedCount) {
224
- const projection = {
225
- records: records.map((record) => ({ sourcePath: record.path, frontmatter: record.frontmatter, body: record.body })),
226
- corpusFindings,
227
- corpusHealth: { recordCount, excludedCount }
228
- };
229
- return createHash2("sha256").update(canonicalStringify(projection), "utf8").digest("hex");
230
- }
231
210
  async function loadCorpusProjection(options) {
232
211
  const roots = await verifyRoots(options);
233
212
  let candidates;
@@ -843,13 +822,9 @@ function registerGetDecision(server, config) {
843
822
  }
844
823
 
845
824
  // src/tools/get-decision-context.ts
846
- import { resolveAffects } from "@adrkit/core";
825
+ import { decisionBucketFor, resolveAffects } from "@adrkit/core";
847
826
  function bucketFor(status) {
848
- if (status === "accepted")
849
- return "governing";
850
- if (status === "draft" || status === "proposed")
851
- return "activeProposals";
852
- return "history";
827
+ return decisionBucketFor(status);
853
828
  }
854
829
  function contextEntry(record, firedMatchers) {
855
830
  return { ...toSummary(record), firedMatchers, relations: toRelationRefs(record.frontmatter) };
@@ -1016,7 +991,7 @@ function registerListSuperseded(server, config) {
1016
991
  }
1017
992
 
1018
993
  // src/server.ts
1019
- var SERVER_INFO = { name: "@adrkit/mcp", version: "0.1.0" };
994
+ var SERVER_INFO = { name: "@adrkit/mcp", version: "0.2.1" };
1020
995
  function buildRegisteredServer(config) {
1021
996
  const server = new McpServer(SERVER_INFO);
1022
997
  registerSearchDecisions(server, config);
package/dist/server.d.ts CHANGED
@@ -10,7 +10,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
10
  import type { ToolConfig } from './tools/shared.js';
11
11
  export declare const SERVER_INFO: {
12
12
  readonly name: "@adrkit/mcp";
13
- readonly version: "0.1.0";
13
+ readonly version: "0.2.1";
14
14
  };
15
15
  /** Package-internal: build the concrete server with exactly the four ratified tools. */
16
16
  export declare function buildRegisteredServer(config: ToolConfig): McpServer;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@adrkit/mcp",
3
- "version": "0.2.0",
3
+ "mcpName": "dev.adrkit/mcp",
4
+ "version": "0.2.1",
4
5
  "description": "Local, read-only Model Context Protocol server exposing adrkit decision retrieval over stdio.",
5
6
  "type": "module",
6
7
  "license": "Apache-2.0",
@@ -44,7 +45,7 @@
44
45
  "typecheck": "tsc --noEmit --customConditions bun --project ../../tsconfig.json"
45
46
  },
46
47
  "dependencies": {
47
- "@adrkit/core": "0.2.0",
48
+ "@adrkit/core": "0.2.1",
48
49
  "@modelcontextprotocol/sdk": "1.29.0",
49
50
  "zod": "^4"
50
51
  },
@@ -1,41 +1,16 @@
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
8
 
6
- import type { Finding } from '@adrkit/core';
7
-
8
- /** The sole code-unit comparator: `a < b ? -1 : a > b ? 1 : 0` over UTF-16 units. */
9
- export function compareCodeUnits(a: string, b: string): number {
10
- return a < b ? -1 : a > b ? 1 : 0;
11
- }
12
-
13
- export interface OrderedSummary {
14
- readonly id: string;
15
- readonly sourcePath: string;
16
- }
17
-
18
- /** Canonical `(id, sourcePath)` ascending order; sourcePath is the unique tiebreak. */
19
- export function compareByIdThenPath(a: OrderedSummary, b: OrderedSummary): number {
20
- return compareCodeUnits(a.id, b.id) || compareCodeUnits(a.sourcePath, b.sourcePath);
21
- }
22
-
23
- /** Canonical finding order using `sortFindings`' field tuple with the code-unit comparator. */
24
- export function compareFindings(a: Finding, b: Finding): number {
25
- return (
26
- compareCodeUnits(a.rule, b.rule) ||
27
- compareCodeUnits(a.id ?? '', b.id ?? '') ||
28
- compareCodeUnits(a.pattern ?? '', b.pattern ?? '') ||
29
- compareCodeUnits(a.path ?? '', b.path ?? '') ||
30
- compareCodeUnits(a.field ?? '', b.field ?? '') ||
31
- compareCodeUnits(a.message, b.message)
32
- );
33
- }
34
-
35
- export function sortFindingsCanonical(findings: readonly Finding[]): Finding[] {
36
- return [...findings].sort(compareFindings);
37
- }
38
-
39
- export function sortByIdThenPath<T extends OrderedSummary>(items: readonly T[]): T[] {
40
- return [...items].sort(compareByIdThenPath);
41
- }
9
+ export {
10
+ compareByIdThenPath,
11
+ compareCodeUnits,
12
+ compareFindings,
13
+ sortByIdThenPath,
14
+ sortFindingsCanonical,
15
+ type OrderedSummary,
16
+ } from '@adrkit/core';
@@ -8,9 +8,15 @@
8
8
  */
9
9
 
10
10
  import { access, constants as FS, lstat, realpath, stat } from 'node:fs/promises';
11
- import { createHash } from 'node:crypto';
12
11
  import { isAbsolute, relative, resolve, sep } from 'node:path';
13
- import { discoverAdrFiles, lintCorpus, normalizeDisplayPath, type Adr, type Finding } from '@adrkit/core';
12
+ import {
13
+ discoverAdrFiles,
14
+ fingerprintOf,
15
+ lintCorpus,
16
+ normalizeDisplayPath,
17
+ type Adr,
18
+ type Finding,
19
+ } from '@adrkit/core';
14
20
  import { compareCodeUnits, sortFindingsCanonical } from './ordering.ts';
15
21
 
16
22
  export const MAX_SOURCE_BYTES = 64 * 1024;
@@ -169,31 +175,6 @@ async function verifyRoots(options: LoadCorpusProjectionOptions): Promise<Canoni
169
175
  return roots;
170
176
  }
171
177
 
172
- function canonicalStringify(value: unknown): string {
173
- if (value === null || value === undefined) return 'null';
174
- if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') {
175
- return JSON.stringify(value);
176
- }
177
- if (Array.isArray(value)) return `[${value.map(canonicalStringify).join(',')}]`;
178
- if (typeof value === 'object') {
179
- const record = value as Record<string, unknown>;
180
- const keys = Object.keys(record)
181
- .filter((key) => record[key] !== undefined)
182
- .sort(compareCodeUnits);
183
- return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalStringify(record[key])}`).join(',')}}`;
184
- }
185
- return 'null';
186
- }
187
-
188
- function fingerprintOf(records: readonly Adr[], corpusFindings: readonly Finding[], recordCount: number, excludedCount: number): string {
189
- const projection = {
190
- records: records.map((record) => ({ sourcePath: record.path, frontmatter: record.frontmatter, body: record.body })),
191
- corpusFindings,
192
- corpusHealth: { recordCount, excludedCount },
193
- };
194
- return createHash('sha256').update(canonicalStringify(projection), 'utf8').digest('hex');
195
- }
196
-
197
178
  /** The one entry point every tool handler calls, fresh, at the start of its execution. */
198
179
  export async function loadCorpusProjection(options: LoadCorpusProjectionOptions): Promise<CorpusProjection> {
199
180
  const roots = await verifyRoots(options);
package/src/server.ts CHANGED
@@ -14,7 +14,7 @@ import { registerGetDecisionContext } from './tools/get-decision-context.ts';
14
14
  import { registerListSuperseded } from './tools/list-superseded.ts';
15
15
  import type { ToolConfig } from './tools/shared.ts';
16
16
 
17
- export const SERVER_INFO = { name: '@adrkit/mcp', version: '0.1.0' } as const;
17
+ export const SERVER_INFO = { name: '@adrkit/mcp', version: '0.2.1' } as const;
18
18
 
19
19
  /** Package-internal: build the concrete server with exactly the four ratified tools. */
20
20
  export function buildRegisteredServer(config: ToolConfig): McpServer {
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
- import { resolveAffects, type Adr, type Finding, type FiredMatcher } from '@adrkit/core';
11
+ import { decisionBucketFor, resolveAffects, type Adr, type Finding, type FiredMatcher } from '@adrkit/core';
12
12
  import { compareCodeUnits, sortFindingsCanonical } from '../corpus/ordering.ts';
13
13
  import { paginate, queryShapeHash } from '../pagination/cursor.ts';
14
14
  import {
@@ -37,10 +37,12 @@ interface GetDecisionContextArgs {
37
37
 
38
38
  type Bucket = 'governing' | 'activeProposals' | 'history';
39
39
 
40
+ /**
41
+ * Delegates to `@adrkit/core`'s `decisionBucketFor` so this tool and the CLI/Action
42
+ * cannot drift apart on what counts as governing (#39).
43
+ */
40
44
  function bucketFor(status: string): Bucket {
41
- if (status === 'accepted') return 'governing';
42
- if (status === 'draft' || status === 'proposed') return 'activeProposals';
43
- return 'history';
45
+ return decisionBucketFor(status);
44
46
  }
45
47
 
46
48
  function contextEntry(record: Adr, firedMatchers: readonly FiredMatcher[]): ContextEntry {