@adrkit/mcp 0.2.1 → 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
@@ -16,8 +16,10 @@ agents stop re-proposing paths the team already ruled out.
16
16
 
17
17
  Part of [adrkit](https://adrkit.dev). The corpus lives in git as one Markdown file
18
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).
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.
21
23
 
22
24
  ## Maturity
23
25
 
@@ -40,6 +42,30 @@ npx -y @adrkit/mcp --cwd /path/to/your/repo --dir docs/adr
40
42
  It speaks JSON-RPC over stdio, so you normally point an MCP client at it rather than
41
43
  running it by hand. Copy-pasteable client configs follow.
42
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
+
43
69
  ### Claude Desktop
44
70
 
45
71
  Edit `claude_desktop_config.json` (macOS:
@@ -282,7 +308,7 @@ the underlying SDK server, its registrations, or its transport:
282
308
  import { createAdrkitMcpServer } from '@adrkit/mcp';
283
309
 
284
310
  const server = createAdrkitMcpServer({ cwd: process.cwd(), dir: 'docs/adr' });
285
- await server.start(); // validates the root, connects exactly one stdio transport
311
+ await server.start(); // validates the root, then serves one stdio connection
286
312
  // ... later:
287
313
  await server.close();
288
314
  ```
@@ -290,6 +316,12 @@ await server.close();
290
316
  `createAdrkitMcpServer(options?)` performs no filesystem access at construction and
291
317
  returns a frozen, null-prototype handle with exactly `start()` and `close()`.
292
318
 
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.
324
+
293
325
  ## Limits
294
326
 
295
327
  `query` 1–256 code units (non-empty after trimming); `ref` 1–128; `files[]` 1–256
@@ -323,6 +355,6 @@ traversal.
323
355
 
324
356
  Developed and tested with Bun; published artifacts are ESM targeting Node.js `>=22`
325
357
  and are verified on Node 22 and 24. Runtime dependencies are exactly `@adrkit/core`,
326
- `@modelcontextprotocol/sdk`, and `zod`.
358
+ `@modelcontextprotocol/server` (MCP TypeScript SDK v2), and `zod`.
327
359
 
328
360
  Apache-2.0.
package/dist/bin.js CHANGED
@@ -1,11 +1,11 @@
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
11
  import {
@@ -480,7 +480,7 @@ function searchDecisionsOutputSchema() {
480
480
  sourcePath: z.string(),
481
481
  matchedFields: z.array(z.enum(["id", "title", "tag", "body"]))
482
482
  });
483
- return {
483
+ return z.object({
484
484
  corpusHealth: corpusHealthSchema().optional(),
485
485
  result: z.discriminatedUnion("outcome", [
486
486
  z.object({
@@ -492,7 +492,7 @@ function searchDecisionsOutputSchema() {
492
492
  invalidCursorSchema(),
493
493
  corpusUnavailableSchema()
494
494
  ])
495
- };
495
+ });
496
496
  }
497
497
  function getDecisionOutputSchema() {
498
498
  const fullDecision = z.object({
@@ -504,7 +504,7 @@ function getDecisionOutputSchema() {
504
504
  frontmatter: AdrFrontmatter,
505
505
  body: z.string()
506
506
  });
507
- return {
507
+ return z.object({
508
508
  corpusHealth: corpusHealthSchema().optional(),
509
509
  result: z.discriminatedUnion("outcome", [
510
510
  z.object({ outcome: z.literal("found"), decision: fullDecision, findings: findingsPageSchema() }),
@@ -526,7 +526,7 @@ function getDecisionOutputSchema() {
526
526
  invalidCursorSchema(),
527
527
  corpusUnavailableSchema()
528
528
  ])
529
- };
529
+ });
530
530
  }
531
531
  function getDecisionContextOutputSchema() {
532
532
  const contextEntry = z.object({
@@ -537,7 +537,7 @@ function getDecisionContextOutputSchema() {
537
537
  firedMatchers: z.array(z.object({ type: z.string(), pattern: z.string() })),
538
538
  relations: relationRefsSchema()
539
539
  });
540
- return {
540
+ return z.object({
541
541
  corpusHealth: corpusHealthSchema().optional(),
542
542
  result: z.discriminatedUnion("outcome", [
543
543
  z.object({
@@ -551,7 +551,7 @@ function getDecisionContextOutputSchema() {
551
551
  invalidCursorSchema(),
552
552
  corpusUnavailableSchema()
553
553
  ])
554
- };
554
+ });
555
555
  }
556
556
  function listSupersededOutputSchema() {
557
557
  const supersededBy = z.union([
@@ -578,7 +578,7 @@ function listSupersededOutputSchema() {
578
578
  sourcePath: z.string(),
579
579
  supersededBy
580
580
  });
581
- return {
581
+ return z.object({
582
582
  corpusHealth: corpusHealthSchema().optional(),
583
583
  result: z.discriminatedUnion("outcome", [
584
584
  z.object({
@@ -590,7 +590,7 @@ function listSupersededOutputSchema() {
590
590
  invalidCursorSchema(),
591
591
  corpusUnavailableSchema()
592
592
  ])
593
- };
593
+ });
594
594
  }
595
595
  function structuredResult(result, text, corpusHealth) {
596
596
  const structuredContent = corpusHealth === undefined ? { result } : { corpusHealth, result };
@@ -993,21 +993,30 @@ function registerListSuperseded(server, config) {
993
993
  }
994
994
 
995
995
  // src/server.ts
996
- var SERVER_INFO = { name: "@adrkit/mcp", version: "0.2.1" };
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
+ };
997
1001
  function buildRegisteredServer(config) {
998
- const server = new McpServer(SERVER_INFO);
999
- registerSearchDecisions(server, config);
1002
+ const server = new McpServer(SERVER_INFO, { cacheHints: CACHE_HINTS });
1000
1003
  registerGetDecision(server, config);
1001
1004
  registerGetDecisionContext(server, config);
1002
1005
  registerListSuperseded(server, config);
1006
+ registerSearchDecisions(server, config);
1003
1007
  return server;
1004
1008
  }
1005
1009
 
1006
1010
  // src/index.ts
1011
+ function writeTransportDiagnostic(error) {
1012
+ process.stderr.write(`adrkit-mcp: transport error: ${error.message}
1013
+ `);
1014
+ }
1007
1015
  function createAdrkitMcpServer(options) {
1008
1016
  const cwd = resolve2(options?.cwd ?? process.cwd());
1009
1017
  const dir = options?.dir ?? "docs/adr";
1010
- let server;
1018
+ const onError = options?.onError ?? writeTransportDiagnostic;
1019
+ let connection;
1011
1020
  let startPromise;
1012
1021
  let closePromise;
1013
1022
  let closed = false;
@@ -1018,7 +1027,6 @@ function createAdrkitMcpServer(options) {
1018
1027
  if (startPromise)
1019
1028
  return startPromise;
1020
1029
  startPromise = (async () => {
1021
- let nextServer;
1022
1030
  try {
1023
1031
  const roots = await resolveCanonicalRoots({ cwd, dir });
1024
1032
  const config = {
@@ -1027,19 +1035,9 @@ function createAdrkitMcpServer(options) {
1027
1035
  expectedCanonicalCwd: roots.canonicalCwd,
1028
1036
  maxSourceBytes: MAX_SOURCE_BYTES
1029
1037
  };
1030
- nextServer = buildRegisteredServer(config);
1031
- server = nextServer;
1032
- await nextServer.connect(new StdioServerTransport);
1038
+ connection = serveStdio(() => buildRegisteredServer(config), { onerror: onError });
1033
1039
  } catch (error) {
1034
- server = undefined;
1035
- if (nextServer) {
1036
- try {
1037
- await nextServer.close();
1038
- } catch (closeError) {
1039
- startPromise = undefined;
1040
- throw new AggregateError([error, closeError], "MCP server startup and cleanup failed");
1041
- }
1042
- }
1040
+ connection = undefined;
1043
1041
  startPromise = undefined;
1044
1042
  throw error;
1045
1043
  }
@@ -1053,8 +1051,8 @@ function createAdrkitMcpServer(options) {
1053
1051
  closePromise = (async () => {
1054
1052
  if (startPromise)
1055
1053
  await startPromise;
1056
- const current = server;
1057
- server = undefined;
1054
+ const current = connection;
1055
+ connection = undefined;
1058
1056
  if (current)
1059
1057
  await current.close();
1060
1058
  })();
@@ -1088,6 +1086,13 @@ function reportUnhandledRejection(reason, write = writeStderr, fail2 = () => {
1088
1086
  `);
1089
1087
  fail2();
1090
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
+ }
1091
1096
  async function main(argv, env) {
1092
1097
  let values;
1093
1098
  try {
@@ -1114,7 +1119,7 @@ ${USAGE}`);
1114
1119
  }
1115
1120
  throw error;
1116
1121
  }
1117
- const handle = createAdrkitMcpServer({ cwd, dir });
1122
+ const handle = createAdrkitMcpServer({ cwd, dir, onError: (error) => reportTransportError(error) });
1118
1123
  const shutdown = () => {
1119
1124
  handle.close().finally(() => process.exit(0));
1120
1125
  };
package/dist/index.d.ts CHANGED
@@ -9,6 +9,22 @@
9
9
  export interface AdrkitMcpServerOptions {
10
10
  readonly cwd: string;
11
11
  readonly dir: string;
12
+ /**
13
+ * Called for out-of-band transport failures: a transport that fails to start,
14
+ * and every background error the connection reports afterwards (an stdin or
15
+ * stdout stream error, such as the EPIPE from a client that has gone away).
16
+ *
17
+ * This is not optional plumbing. `serveStdio` reports these **only** through
18
+ * its `onerror` callback — it consumes the rejected `start()` promise
19
+ * deliberately — so without a callback a broken transport tears the connection
20
+ * down while the process still exits 0. That is a dead server reporting
21
+ * success, the fail-quiet shape ADR-0016 rejects, so the default writes a
22
+ * diagnostic to stderr rather than staying silent. `main-module.ts` supplies a
23
+ * reporter that also fails the exit status.
24
+ *
25
+ * Never writes to stdout: that is reserved for protocol frames.
26
+ */
27
+ readonly onError: (error: Error) => void;
12
28
  }
13
29
  export interface AdrkitMcpServerHandle {
14
30
  start(): Promise<void>;
@@ -16,8 +32,14 @@ export interface AdrkitMcpServerHandle {
16
32
  }
17
33
  /**
18
34
  * The public stdio lifecycle factory. Performs NO filesystem access at construction;
19
- * `start()` validates the configured root, builds the closure-private server, creates
20
- * exactly one `StdioServerTransport`, and connects it. The concrete server, its
35
+ * `start()` validates the configured root, then hands a closure-private server factory
36
+ * to the SDK's connection-pinned `serveStdio` entry. The concrete server, its
21
37
  * registrations, and its transport remain unreachable to the caller.
38
+ *
39
+ * `serveStdio` — not a hand-wired `StdioServerTransport` — is what makes this server
40
+ * speak protocol revision 2026-07-28. The opening exchange selects the connection's
41
+ * era and pins one factory instance to it; `legacy: 'serve'` (the default) keeps
42
+ * 2025-era clients working unchanged. The four tools are registered once and served
43
+ * identically to both eras.
22
44
  */
23
45
  export declare function createAdrkitMcpServer(options?: Partial<AdrkitMcpServerOptions>): Readonly<AdrkitMcpServerHandle>;
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/index.ts
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
3
3
  import { resolve as resolve2 } from "node:path";
4
4
 
5
5
  // src/server.ts
6
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { McpServer } from "@modelcontextprotocol/server";
7
7
 
8
8
  // src/corpus/ordering.ts
9
9
  import {
@@ -478,7 +478,7 @@ function searchDecisionsOutputSchema() {
478
478
  sourcePath: z.string(),
479
479
  matchedFields: z.array(z.enum(["id", "title", "tag", "body"]))
480
480
  });
481
- return {
481
+ return z.object({
482
482
  corpusHealth: corpusHealthSchema().optional(),
483
483
  result: z.discriminatedUnion("outcome", [
484
484
  z.object({
@@ -490,7 +490,7 @@ function searchDecisionsOutputSchema() {
490
490
  invalidCursorSchema(),
491
491
  corpusUnavailableSchema()
492
492
  ])
493
- };
493
+ });
494
494
  }
495
495
  function getDecisionOutputSchema() {
496
496
  const fullDecision = z.object({
@@ -502,7 +502,7 @@ function getDecisionOutputSchema() {
502
502
  frontmatter: AdrFrontmatter,
503
503
  body: z.string()
504
504
  });
505
- return {
505
+ return z.object({
506
506
  corpusHealth: corpusHealthSchema().optional(),
507
507
  result: z.discriminatedUnion("outcome", [
508
508
  z.object({ outcome: z.literal("found"), decision: fullDecision, findings: findingsPageSchema() }),
@@ -524,7 +524,7 @@ function getDecisionOutputSchema() {
524
524
  invalidCursorSchema(),
525
525
  corpusUnavailableSchema()
526
526
  ])
527
- };
527
+ });
528
528
  }
529
529
  function getDecisionContextOutputSchema() {
530
530
  const contextEntry = z.object({
@@ -535,7 +535,7 @@ function getDecisionContextOutputSchema() {
535
535
  firedMatchers: z.array(z.object({ type: z.string(), pattern: z.string() })),
536
536
  relations: relationRefsSchema()
537
537
  });
538
- return {
538
+ return z.object({
539
539
  corpusHealth: corpusHealthSchema().optional(),
540
540
  result: z.discriminatedUnion("outcome", [
541
541
  z.object({
@@ -549,7 +549,7 @@ function getDecisionContextOutputSchema() {
549
549
  invalidCursorSchema(),
550
550
  corpusUnavailableSchema()
551
551
  ])
552
- };
552
+ });
553
553
  }
554
554
  function listSupersededOutputSchema() {
555
555
  const supersededBy = z.union([
@@ -576,7 +576,7 @@ function listSupersededOutputSchema() {
576
576
  sourcePath: z.string(),
577
577
  supersededBy
578
578
  });
579
- return {
579
+ return z.object({
580
580
  corpusHealth: corpusHealthSchema().optional(),
581
581
  result: z.discriminatedUnion("outcome", [
582
582
  z.object({
@@ -588,7 +588,7 @@ function listSupersededOutputSchema() {
588
588
  invalidCursorSchema(),
589
589
  corpusUnavailableSchema()
590
590
  ])
591
- };
591
+ });
592
592
  }
593
593
  function structuredResult(result, text, corpusHealth) {
594
594
  const structuredContent = corpusHealth === undefined ? { result } : { corpusHealth, result };
@@ -991,21 +991,30 @@ function registerListSuperseded(server, config) {
991
991
  }
992
992
 
993
993
  // src/server.ts
994
- var SERVER_INFO = { name: "@adrkit/mcp", version: "0.2.1" };
994
+ var SERVER_INFO = { name: "@adrkit/mcp", version: "0.3.0" };
995
+ var CACHE_HINTS = {
996
+ "tools/list": { ttlMs: 300000, cacheScope: "public" },
997
+ "server/discover": { ttlMs: 300000, cacheScope: "public" }
998
+ };
995
999
  function buildRegisteredServer(config) {
996
- const server = new McpServer(SERVER_INFO);
997
- registerSearchDecisions(server, config);
1000
+ const server = new McpServer(SERVER_INFO, { cacheHints: CACHE_HINTS });
998
1001
  registerGetDecision(server, config);
999
1002
  registerGetDecisionContext(server, config);
1000
1003
  registerListSuperseded(server, config);
1004
+ registerSearchDecisions(server, config);
1001
1005
  return server;
1002
1006
  }
1003
1007
 
1004
1008
  // src/index.ts
1009
+ function writeTransportDiagnostic(error) {
1010
+ process.stderr.write(`adrkit-mcp: transport error: ${error.message}
1011
+ `);
1012
+ }
1005
1013
  function createAdrkitMcpServer(options) {
1006
1014
  const cwd = resolve2(options?.cwd ?? process.cwd());
1007
1015
  const dir = options?.dir ?? "docs/adr";
1008
- let server;
1016
+ const onError = options?.onError ?? writeTransportDiagnostic;
1017
+ let connection;
1009
1018
  let startPromise;
1010
1019
  let closePromise;
1011
1020
  let closed = false;
@@ -1016,7 +1025,6 @@ function createAdrkitMcpServer(options) {
1016
1025
  if (startPromise)
1017
1026
  return startPromise;
1018
1027
  startPromise = (async () => {
1019
- let nextServer;
1020
1028
  try {
1021
1029
  const roots = await resolveCanonicalRoots({ cwd, dir });
1022
1030
  const config = {
@@ -1025,19 +1033,9 @@ function createAdrkitMcpServer(options) {
1025
1033
  expectedCanonicalCwd: roots.canonicalCwd,
1026
1034
  maxSourceBytes: MAX_SOURCE_BYTES
1027
1035
  };
1028
- nextServer = buildRegisteredServer(config);
1029
- server = nextServer;
1030
- await nextServer.connect(new StdioServerTransport);
1036
+ connection = serveStdio(() => buildRegisteredServer(config), { onerror: onError });
1031
1037
  } catch (error) {
1032
- server = undefined;
1033
- if (nextServer) {
1034
- try {
1035
- await nextServer.close();
1036
- } catch (closeError) {
1037
- startPromise = undefined;
1038
- throw new AggregateError([error, closeError], "MCP server startup and cleanup failed");
1039
- }
1040
- }
1038
+ connection = undefined;
1041
1039
  startPromise = undefined;
1042
1040
  throw error;
1043
1041
  }
@@ -1051,8 +1049,8 @@ function createAdrkitMcpServer(options) {
1051
1049
  closePromise = (async () => {
1052
1050
  if (startPromise)
1053
1051
  await startPromise;
1054
- const current = server;
1055
- server = undefined;
1052
+ const current = connection;
1053
+ connection = undefined;
1056
1054
  if (current)
1057
1055
  await current.close();
1058
1056
  })();
@@ -8,4 +8,13 @@
8
8
  */
9
9
  export declare function isMainModule(moduleUrl: string, argvPath: string | undefined): boolean;
10
10
  export declare function reportUnhandledRejection(reason: unknown, write?: (text: string) => void, fail?: () => void): void;
11
+ /**
12
+ * Out-of-band transport failures reach the bin here.
13
+ *
14
+ * `serveStdio` reports them only through its `onerror` callback — it consumes
15
+ * the rejected `start()` promise itself — so this is the only path by which a
16
+ * broken transport can reach stderr and a non-zero exit status. Without it the
17
+ * connection tears down while the process still exits 0 (ADR-0016).
18
+ */
19
+ export declare function reportTransportError(error: Error, write?: (text: string) => void, fail?: () => void): void;
11
20
  export declare function main(argv: string[], env: Record<string, string | undefined>): Promise<0 | 1 | 2>;
package/dist/server.d.ts CHANGED
@@ -6,11 +6,25 @@
6
6
  * exposes only the sealed lifecycle handle. This module is absent from
7
7
  * `package.json#exports` and every public subpath.
8
8
  */
9
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { McpServer } from '@modelcontextprotocol/server';
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.2.1";
13
+ readonly version: "0.3.0";
14
14
  };
15
- /** Package-internal: build the concrete server with exactly the four ratified tools. */
15
+ /**
16
+ * The MCP protocol revision this server serves through `serveStdio`'s modern era.
17
+ *
18
+ * The SDK keeps the revision string internal (`LATEST_PROTOCOL_VERSION` names the
19
+ * latest *legacy*-era version, `2025-11-25`), so the modern revision is stated here
20
+ * once and asserted against the wire in `test/bin.test.ts`.
21
+ */
22
+ export declare const MODERN_PROTOCOL_VERSION: "2026-07-28";
23
+ /**
24
+ * Package-internal: build the concrete server with exactly the four ratified tools.
25
+ *
26
+ * Registration order is lexicographic by tool name so `tools/list` answers in a
27
+ * deterministic, self-evidently stable order (2026-07-28 minor change 3 — servers
28
+ * SHOULD do this so clients can cache catalogs and keep upstream prompt caches warm).
29
+ */
16
30
  export declare function buildRegisteredServer(config: ToolConfig): McpServer;
@@ -6,6 +6,6 @@
6
6
  * reading a caller-supplied path. One canonical flat walk is paginated, then the
7
7
  * page is partitioned by status.
8
8
  */
9
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import type { McpServer } from '@modelcontextprotocol/server';
10
10
  import { type ToolConfig } from './shared.js';
11
11
  export declare function registerGetDecisionContext(server: McpServer, config: ToolConfig): void;
@@ -6,6 +6,6 @@
6
6
  * resolved through the fresh local `byId` bucket into found / not-found /
7
7
  * ambiguous-local-id. Relation refs are surfaced verbatim, never expanded.
8
8
  */
9
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import type { McpServer } from '@modelcontextprotocol/server';
10
10
  import { type ToolConfig } from './shared.js';
11
11
  export declare function registerGetDecision(server: McpServer, config: ToolConfig): void;
@@ -6,6 +6,6 @@
6
6
  * lineage, never embeds candidate arrays, and mints only the two specified derived
7
7
  * finding templates.
8
8
  */
9
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import type { McpServer } from '@modelcontextprotocol/server';
10
10
  import { type ToolConfig } from './shared.js';
11
11
  export declare function registerListSuperseded(server: McpServer, config: ToolConfig): void;
@@ -6,6 +6,6 @@
6
6
  * normalizer. Graveyard records are included by default. Returns bounded summaries
7
7
  * only — never a body, ranking score, or hidden index.
8
8
  */
9
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import type { McpServer } from '@modelcontextprotocol/server';
10
10
  import { type ToolConfig } from './shared.js';
11
11
  export declare function registerSearchDecisions(server: McpServer, config: ToolConfig): void;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { z } from 'zod';
8
8
  import { AdrFrontmatter, Status, Scope, type Finding, type FiredMatcher } from '@adrkit/core';
9
- import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
9
+ import type { CallToolResult } from '@modelcontextprotocol/server';
10
10
  import { type CorpusHealth, type CorpusProjection, type CorpusUnavailableReason } from '../corpus/projection.js';
11
11
  import type { InvalidCursorReason, Page } from '../pagination/cursor.js';
12
12
  export type { Finding, FiredMatcher } from '@adrkit/core';
@@ -215,7 +215,7 @@ export type TextSpec = {
215
215
  export declare function cap512(value: string): string;
216
216
  export declare function renderResponseText(spec: TextSpec): string;
217
217
  export type ToolInputSchema = z.ZodType;
218
- export type ToolOutputSchema = z.ZodRawShape;
218
+ export type ToolOutputSchema = z.ZodType;
219
219
  export declare function searchDecisionsInputSchema(): ToolInputSchema;
220
220
  export declare function getDecisionInputSchema(): ToolInputSchema;
221
221
  export declare function getDecisionContextInputSchema(): ToolInputSchema;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adrkit/mcp",
3
3
  "mcpName": "dev.adrkit/mcp",
4
- "version": "0.2.1",
4
+ "version": "0.3.0",
5
5
  "description": "Local, read-only Model Context Protocol server exposing adrkit decision retrieval over stdio.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",
@@ -45,11 +45,12 @@
45
45
  "typecheck": "tsc --noEmit --customConditions bun --project ../../tsconfig.json"
46
46
  },
47
47
  "dependencies": {
48
- "@adrkit/core": "0.2.1",
49
- "@modelcontextprotocol/sdk": "1.29.0",
50
- "zod": "^4"
48
+ "@adrkit/core": "0.3.0",
49
+ "@modelcontextprotocol/server": "2.0.0",
50
+ "zod": "^4.2.0"
51
51
  },
52
52
  "devDependencies": {
53
+ "@modelcontextprotocol/client": "2.0.0",
53
54
  "@types/bun": "latest"
54
55
  },
55
56
  "files": [
package/src/index.ts CHANGED
@@ -7,8 +7,7 @@
7
7
  * construction time (data-model.md §8, contracts/tools.md §1).
8
8
  */
9
9
 
10
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { serveStdio, type StdioServerHandle } from '@modelcontextprotocol/server/stdio';
12
11
  import { resolve } from 'node:path';
13
12
  import { buildRegisteredServer } from './server.ts';
14
13
  import { resolveCanonicalRoots, MAX_SOURCE_BYTES } from './corpus/projection.ts';
@@ -17,6 +16,22 @@ import type { ToolConfig } from './tools/shared.ts';
17
16
  export interface AdrkitMcpServerOptions {
18
17
  readonly cwd: string;
19
18
  readonly dir: string;
19
+ /**
20
+ * Called for out-of-band transport failures: a transport that fails to start,
21
+ * and every background error the connection reports afterwards (an stdin or
22
+ * stdout stream error, such as the EPIPE from a client that has gone away).
23
+ *
24
+ * This is not optional plumbing. `serveStdio` reports these **only** through
25
+ * its `onerror` callback — it consumes the rejected `start()` promise
26
+ * deliberately — so without a callback a broken transport tears the connection
27
+ * down while the process still exits 0. That is a dead server reporting
28
+ * success, the fail-quiet shape ADR-0016 rejects, so the default writes a
29
+ * diagnostic to stderr rather than staying silent. `main-module.ts` supplies a
30
+ * reporter that also fails the exit status.
31
+ *
32
+ * Never writes to stdout: that is reserved for protocol frames.
33
+ */
34
+ readonly onError: (error: Error) => void;
20
35
  }
21
36
 
22
37
  export interface AdrkitMcpServerHandle {
@@ -24,19 +39,30 @@ export interface AdrkitMcpServerHandle {
24
39
  close(): Promise<void>;
25
40
  }
26
41
 
42
+ function writeTransportDiagnostic(error: Error): void {
43
+ process.stderr.write(`adrkit-mcp: transport error: ${error.message}\n`);
44
+ }
45
+
27
46
  /**
28
47
  * The public stdio lifecycle factory. Performs NO filesystem access at construction;
29
- * `start()` validates the configured root, builds the closure-private server, creates
30
- * exactly one `StdioServerTransport`, and connects it. The concrete server, its
48
+ * `start()` validates the configured root, then hands a closure-private server factory
49
+ * to the SDK's connection-pinned `serveStdio` entry. The concrete server, its
31
50
  * registrations, and its transport remain unreachable to the caller.
51
+ *
52
+ * `serveStdio` — not a hand-wired `StdioServerTransport` — is what makes this server
53
+ * speak protocol revision 2026-07-28. The opening exchange selects the connection's
54
+ * era and pins one factory instance to it; `legacy: 'serve'` (the default) keeps
55
+ * 2025-era clients working unchanged. The four tools are registered once and served
56
+ * identically to both eras.
32
57
  */
33
58
  export function createAdrkitMcpServer(
34
59
  options?: Partial<AdrkitMcpServerOptions>,
35
60
  ): Readonly<AdrkitMcpServerHandle> {
36
61
  const cwd = resolve(options?.cwd ?? process.cwd());
37
62
  const dir = options?.dir ?? 'docs/adr';
63
+ const onError = options?.onError ?? writeTransportDiagnostic;
38
64
 
39
- let server: McpServer | undefined;
65
+ let connection: StdioServerHandle | undefined;
40
66
  let startPromise: Promise<void> | undefined;
41
67
  let closePromise: Promise<void> | undefined;
42
68
  let closed = false;
@@ -48,7 +74,6 @@ export function createAdrkitMcpServer(
48
74
  if (startPromise) return startPromise;
49
75
 
50
76
  startPromise = (async () => {
51
- let nextServer: McpServer | undefined;
52
77
  try {
53
78
  const roots = await resolveCanonicalRoots({ cwd, dir });
54
79
  const config: ToolConfig = {
@@ -57,19 +82,9 @@ export function createAdrkitMcpServer(
57
82
  expectedCanonicalCwd: roots.canonicalCwd,
58
83
  maxSourceBytes: MAX_SOURCE_BYTES,
59
84
  };
60
- nextServer = buildRegisteredServer(config);
61
- server = nextServer;
62
- await nextServer.connect(new StdioServerTransport());
85
+ connection = serveStdio(() => buildRegisteredServer(config), { onerror: onError });
63
86
  } catch (error) {
64
- server = undefined;
65
- if (nextServer) {
66
- try {
67
- await nextServer.close();
68
- } catch (closeError) {
69
- startPromise = undefined;
70
- throw new AggregateError([error, closeError], 'MCP server startup and cleanup failed');
71
- }
72
- }
87
+ connection = undefined;
73
88
  startPromise = undefined;
74
89
  throw error;
75
90
  }
@@ -82,8 +97,8 @@ export function createAdrkitMcpServer(
82
97
  closed = true;
83
98
  closePromise = (async () => {
84
99
  if (startPromise) await startPromise;
85
- const current = server;
86
- server = undefined;
100
+ const current = connection;
101
+ connection = undefined;
87
102
  if (current) await current.close();
88
103
  })();
89
104
  return closePromise;
@@ -37,6 +37,25 @@ export function reportUnhandledRejection(
37
37
  fail();
38
38
  }
39
39
 
40
+ /**
41
+ * Out-of-band transport failures reach the bin here.
42
+ *
43
+ * `serveStdio` reports them only through its `onerror` callback — it consumes
44
+ * the rejected `start()` promise itself — so this is the only path by which a
45
+ * broken transport can reach stderr and a non-zero exit status. Without it the
46
+ * connection tears down while the process still exits 0 (ADR-0016).
47
+ */
48
+ export function reportTransportError(
49
+ error: Error,
50
+ write: (text: string) => void = writeStderr,
51
+ fail: () => void = () => {
52
+ process.exitCode = 1;
53
+ },
54
+ ): void {
55
+ write(`adrkit-mcp: transport error: ${error.message}\n`);
56
+ fail();
57
+ }
58
+
40
59
  export async function main(
41
60
  argv: string[],
42
61
  env: Record<string, string | undefined>,
@@ -68,7 +87,7 @@ export async function main(
68
87
  throw error;
69
88
  }
70
89
 
71
- const handle = createAdrkitMcpServer({ cwd, dir });
90
+ const handle = createAdrkitMcpServer({ cwd, dir, onError: (error) => reportTransportError(error) });
72
91
 
73
92
  const shutdown = (): void => {
74
93
  void handle.close().finally(() => process.exit(0));
package/src/server.ts CHANGED
@@ -7,21 +7,54 @@
7
7
  * `package.json#exports` and every public subpath.
8
8
  */
9
9
 
10
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { McpServer, type ServerOptions } from '@modelcontextprotocol/server';
11
11
  import { registerSearchDecisions } from './tools/search-decisions.ts';
12
12
  import { registerGetDecision } from './tools/get-decision.ts';
13
13
  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.2.1' } as const;
17
+ export const SERVER_INFO = { name: '@adrkit/mcp', version: '0.3.0' } as const;
18
18
 
19
- /** Package-internal: build the concrete server with exactly the four ratified tools. */
19
+ /**
20
+ * The MCP protocol revision this server serves through `serveStdio`'s modern era.
21
+ *
22
+ * The SDK keeps the revision string internal (`LATEST_PROTOCOL_VERSION` names the
23
+ * latest *legacy*-era version, `2025-11-25`), so the modern revision is stated here
24
+ * once and asserted against the wire in `test/bin.test.ts`.
25
+ */
26
+ export const MODERN_PROTOCOL_VERSION = '2026-07-28' as const;
27
+
28
+ /**
29
+ * SEP-2549 cache hints for the two cacheable results this server can answer.
30
+ *
31
+ * Both are immutable for the lifetime of the process and carry no corpus content,
32
+ * caller identity, or per-request state: `tools/list` is the four ratified tools with
33
+ * their fixed schemas and annotations, and `server/discover` is the supported
34
+ * revisions plus the tools capability. They are therefore honestly `public` and safe
35
+ * to cache, which spares an agent a round trip per re-list. Corpus reads are NOT
36
+ * cacheable and are unaffected — every `tools/call` still loads a fresh projection.
37
+ *
38
+ * Without this the SDK falls back to the conservative `{ ttlMs: 0, cacheScope:
39
+ * 'private' }`. 2025-era responses never carry these fields either way.
40
+ */
41
+ const CACHE_HINTS = {
42
+ 'tools/list': { ttlMs: 300_000, cacheScope: 'public' },
43
+ 'server/discover': { ttlMs: 300_000, cacheScope: 'public' },
44
+ } as const satisfies ServerOptions['cacheHints'];
45
+
46
+ /**
47
+ * Package-internal: build the concrete server with exactly the four ratified tools.
48
+ *
49
+ * Registration order is lexicographic by tool name so `tools/list` answers in a
50
+ * deterministic, self-evidently stable order (2026-07-28 minor change 3 — servers
51
+ * SHOULD do this so clients can cache catalogs and keep upstream prompt caches warm).
52
+ */
20
53
  export function buildRegisteredServer(config: ToolConfig): McpServer {
21
- const server = new McpServer(SERVER_INFO);
22
- registerSearchDecisions(server, config);
54
+ const server = new McpServer(SERVER_INFO, { cacheHints: CACHE_HINTS });
23
55
  registerGetDecision(server, config);
24
56
  registerGetDecisionContext(server, config);
25
57
  registerListSuperseded(server, config);
58
+ registerSearchDecisions(server, config);
26
59
  return server;
27
60
  }
@@ -7,7 +7,7 @@
7
7
  * page is partitioned by status.
8
8
  */
9
9
 
10
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import type { McpServer } from '@modelcontextprotocol/server';
11
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';
@@ -7,7 +7,7 @@
7
7
  * ambiguous-local-id. Relation refs are surfaced verbatim, never expanded.
8
8
  */
9
9
 
10
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import type { McpServer } from '@modelcontextprotocol/server';
11
11
  import { parseAdrRef, type Adr } from '@adrkit/core';
12
12
  import { paginate, queryShapeHash, checkInapplicablePrimaryCursor } from '../pagination/cursor.ts';
13
13
  import {
@@ -7,7 +7,7 @@
7
7
  * finding templates.
8
8
  */
9
9
 
10
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import type { McpServer } from '@modelcontextprotocol/server';
11
11
  import { parseAdrRef, type Adr, type Finding } from '@adrkit/core';
12
12
  import { sortFindingsCanonical } from '../corpus/ordering.ts';
13
13
  import { paginate, queryShapeHash } from '../pagination/cursor.ts';
@@ -7,7 +7,7 @@
7
7
  * only — never a body, ranking score, or hidden index.
8
8
  */
9
9
 
10
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import type { McpServer } from '@modelcontextprotocol/server';
11
11
  import type { Adr } from '@adrkit/core';
12
12
  import { compareCodeUnits } from '../corpus/ordering.ts';
13
13
  import { normalize } from '../search/normalize.ts';
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { z } from 'zod';
9
9
  import { AdrFrontmatter, AdrRef, Status, Scope, type Finding, type FiredMatcher } from '@adrkit/core';
10
- import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
10
+ import type { CallToolResult } from '@modelcontextprotocol/server';
11
11
  import {
12
12
  loadCorpusProjection,
13
13
  CorpusUnavailableError,
@@ -265,7 +265,7 @@ export function renderResponseText(spec: TextSpec): string {
265
265
  * ------------------------------------------------------------------ */
266
266
 
267
267
  export type ToolInputSchema = z.ZodType;
268
- export type ToolOutputSchema = z.ZodRawShape;
268
+ export type ToolOutputSchema = z.ZodType;
269
269
 
270
270
  const uniqueArray = <T>(schema: z.ZodType<T>, min: number, max: number) =>
271
271
  z
@@ -392,7 +392,7 @@ export function searchDecisionsOutputSchema(): ToolOutputSchema {
392
392
  sourcePath: z.string(),
393
393
  matchedFields: z.array(z.enum(['id', 'title', 'tag', 'body'])),
394
394
  });
395
- return {
395
+ return z.object({
396
396
  corpusHealth: corpusHealthSchema().optional(),
397
397
  result: z.discriminatedUnion('outcome', [
398
398
  z.object({
@@ -404,7 +404,7 @@ export function searchDecisionsOutputSchema(): ToolOutputSchema {
404
404
  invalidCursorSchema(),
405
405
  corpusUnavailableSchema(),
406
406
  ]),
407
- };
407
+ });
408
408
  }
409
409
 
410
410
  export function getDecisionOutputSchema(): ToolOutputSchema {
@@ -417,7 +417,7 @@ export function getDecisionOutputSchema(): ToolOutputSchema {
417
417
  frontmatter: AdrFrontmatter,
418
418
  body: z.string(),
419
419
  });
420
- return {
420
+ return z.object({
421
421
  corpusHealth: corpusHealthSchema().optional(),
422
422
  result: z.discriminatedUnion('outcome', [
423
423
  z.object({ outcome: z.literal('found'), decision: fullDecision, findings: findingsPageSchema() }),
@@ -439,7 +439,7 @@ export function getDecisionOutputSchema(): ToolOutputSchema {
439
439
  invalidCursorSchema(),
440
440
  corpusUnavailableSchema(),
441
441
  ]),
442
- };
442
+ });
443
443
  }
444
444
 
445
445
  export function getDecisionContextOutputSchema(): ToolOutputSchema {
@@ -451,7 +451,7 @@ export function getDecisionContextOutputSchema(): ToolOutputSchema {
451
451
  firedMatchers: z.array(z.object({ type: z.string(), pattern: z.string() })),
452
452
  relations: relationRefsSchema(),
453
453
  });
454
- return {
454
+ return z.object({
455
455
  corpusHealth: corpusHealthSchema().optional(),
456
456
  result: z.discriminatedUnion('outcome', [
457
457
  z.object({
@@ -465,7 +465,7 @@ export function getDecisionContextOutputSchema(): ToolOutputSchema {
465
465
  invalidCursorSchema(),
466
466
  corpusUnavailableSchema(),
467
467
  ]),
468
- };
468
+ });
469
469
  }
470
470
 
471
471
  export function listSupersededOutputSchema(): ToolOutputSchema {
@@ -493,7 +493,7 @@ export function listSupersededOutputSchema(): ToolOutputSchema {
493
493
  sourcePath: z.string(),
494
494
  supersededBy,
495
495
  });
496
- return {
496
+ return z.object({
497
497
  corpusHealth: corpusHealthSchema().optional(),
498
498
  result: z.discriminatedUnion('outcome', [
499
499
  z.object({
@@ -505,7 +505,7 @@ export function listSupersededOutputSchema(): ToolOutputSchema {
505
505
  invalidCursorSchema(),
506
506
  corpusUnavailableSchema(),
507
507
  ]),
508
- };
508
+ });
509
509
  }
510
510
 
511
511
  /* ------------------------------------------------------------------ *