@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/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,20 +1,18 @@
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
- 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;
@@ -499,7 +478,7 @@ function searchDecisionsOutputSchema() {
499
478
  sourcePath: z.string(),
500
479
  matchedFields: z.array(z.enum(["id", "title", "tag", "body"]))
501
480
  });
502
- return {
481
+ return z.object({
503
482
  corpusHealth: corpusHealthSchema().optional(),
504
483
  result: z.discriminatedUnion("outcome", [
505
484
  z.object({
@@ -511,7 +490,7 @@ function searchDecisionsOutputSchema() {
511
490
  invalidCursorSchema(),
512
491
  corpusUnavailableSchema()
513
492
  ])
514
- };
493
+ });
515
494
  }
516
495
  function getDecisionOutputSchema() {
517
496
  const fullDecision = z.object({
@@ -523,7 +502,7 @@ function getDecisionOutputSchema() {
523
502
  frontmatter: AdrFrontmatter,
524
503
  body: z.string()
525
504
  });
526
- return {
505
+ return z.object({
527
506
  corpusHealth: corpusHealthSchema().optional(),
528
507
  result: z.discriminatedUnion("outcome", [
529
508
  z.object({ outcome: z.literal("found"), decision: fullDecision, findings: findingsPageSchema() }),
@@ -545,7 +524,7 @@ function getDecisionOutputSchema() {
545
524
  invalidCursorSchema(),
546
525
  corpusUnavailableSchema()
547
526
  ])
548
- };
527
+ });
549
528
  }
550
529
  function getDecisionContextOutputSchema() {
551
530
  const contextEntry = z.object({
@@ -556,7 +535,7 @@ function getDecisionContextOutputSchema() {
556
535
  firedMatchers: z.array(z.object({ type: z.string(), pattern: z.string() })),
557
536
  relations: relationRefsSchema()
558
537
  });
559
- return {
538
+ return z.object({
560
539
  corpusHealth: corpusHealthSchema().optional(),
561
540
  result: z.discriminatedUnion("outcome", [
562
541
  z.object({
@@ -570,7 +549,7 @@ function getDecisionContextOutputSchema() {
570
549
  invalidCursorSchema(),
571
550
  corpusUnavailableSchema()
572
551
  ])
573
- };
552
+ });
574
553
  }
575
554
  function listSupersededOutputSchema() {
576
555
  const supersededBy = z.union([
@@ -597,7 +576,7 @@ function listSupersededOutputSchema() {
597
576
  sourcePath: z.string(),
598
577
  supersededBy
599
578
  });
600
- return {
579
+ return z.object({
601
580
  corpusHealth: corpusHealthSchema().optional(),
602
581
  result: z.discriminatedUnion("outcome", [
603
582
  z.object({
@@ -609,7 +588,7 @@ function listSupersededOutputSchema() {
609
588
  invalidCursorSchema(),
610
589
  corpusUnavailableSchema()
611
590
  ])
612
- };
591
+ });
613
592
  }
614
593
  function structuredResult(result, text, corpusHealth) {
615
594
  const structuredContent = corpusHealth === undefined ? { result } : { corpusHealth, result };
@@ -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,21 +991,30 @@ 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.3.0" };
995
+ var CACHE_HINTS = {
996
+ "tools/list": { ttlMs: 300000, cacheScope: "public" },
997
+ "server/discover": { ttlMs: 300000, cacheScope: "public" }
998
+ };
1020
999
  function buildRegisteredServer(config) {
1021
- const server = new McpServer(SERVER_INFO);
1022
- registerSearchDecisions(server, config);
1000
+ const server = new McpServer(SERVER_INFO, { cacheHints: CACHE_HINTS });
1023
1001
  registerGetDecision(server, config);
1024
1002
  registerGetDecisionContext(server, config);
1025
1003
  registerListSuperseded(server, config);
1004
+ registerSearchDecisions(server, config);
1026
1005
  return server;
1027
1006
  }
1028
1007
 
1029
1008
  // src/index.ts
1009
+ function writeTransportDiagnostic(error) {
1010
+ process.stderr.write(`adrkit-mcp: transport error: ${error.message}
1011
+ `);
1012
+ }
1030
1013
  function createAdrkitMcpServer(options) {
1031
1014
  const cwd = resolve2(options?.cwd ?? process.cwd());
1032
1015
  const dir = options?.dir ?? "docs/adr";
1033
- let server;
1016
+ const onError = options?.onError ?? writeTransportDiagnostic;
1017
+ let connection;
1034
1018
  let startPromise;
1035
1019
  let closePromise;
1036
1020
  let closed = false;
@@ -1041,7 +1025,6 @@ function createAdrkitMcpServer(options) {
1041
1025
  if (startPromise)
1042
1026
  return startPromise;
1043
1027
  startPromise = (async () => {
1044
- let nextServer;
1045
1028
  try {
1046
1029
  const roots = await resolveCanonicalRoots({ cwd, dir });
1047
1030
  const config = {
@@ -1050,19 +1033,9 @@ function createAdrkitMcpServer(options) {
1050
1033
  expectedCanonicalCwd: roots.canonicalCwd,
1051
1034
  maxSourceBytes: MAX_SOURCE_BYTES
1052
1035
  };
1053
- nextServer = buildRegisteredServer(config);
1054
- server = nextServer;
1055
- await nextServer.connect(new StdioServerTransport);
1036
+ connection = serveStdio(() => buildRegisteredServer(config), { onerror: onError });
1056
1037
  } catch (error) {
1057
- server = undefined;
1058
- if (nextServer) {
1059
- try {
1060
- await nextServer.close();
1061
- } catch (closeError) {
1062
- startPromise = undefined;
1063
- throw new AggregateError([error, closeError], "MCP server startup and cleanup failed");
1064
- }
1065
- }
1038
+ connection = undefined;
1066
1039
  startPromise = undefined;
1067
1040
  throw error;
1068
1041
  }
@@ -1076,8 +1049,8 @@ function createAdrkitMcpServer(options) {
1076
1049
  closePromise = (async () => {
1077
1050
  if (startPromise)
1078
1051
  await startPromise;
1079
- const current = server;
1080
- server = undefined;
1052
+ const current = connection;
1053
+ connection = undefined;
1081
1054
  if (current)
1082
1055
  await current.close();
1083
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.1.0";
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,6 +1,7 @@
1
1
  {
2
2
  "name": "@adrkit/mcp",
3
- "version": "0.2.0",
3
+ "mcpName": "dev.adrkit/mcp",
4
+ "version": "0.3.0",
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,11 +45,12 @@
44
45
  "typecheck": "tsc --noEmit --customConditions bun --project ../../tsconfig.json"
45
46
  },
46
47
  "dependencies": {
47
- "@adrkit/core": "0.2.0",
48
- "@modelcontextprotocol/sdk": "1.29.0",
49
- "zod": "^4"
48
+ "@adrkit/core": "0.3.0",
49
+ "@modelcontextprotocol/server": "2.0.0",
50
+ "zod": "^4.2.0"
50
51
  },
51
52
  "devDependencies": {
53
+ "@modelcontextprotocol/client": "2.0.0",
52
54
  "@types/bun": "latest"
53
55
  },
54
56
  "files": [
@@ -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/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));