@adrkit/mcp 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -31
- package/dist/bin.js +50 -70
- package/dist/corpus/ordering.d.ts +6 -15
- package/dist/index.d.ts +24 -2
- package/dist/index.js +42 -69
- package/dist/main-module.d.ts +9 -0
- package/dist/server.d.ts +17 -3
- package/dist/tools/get-decision-context.d.ts +1 -1
- package/dist/tools/get-decision.d.ts +1 -1
- package/dist/tools/list-superseded.d.ts +1 -1
- package/dist/tools/search-decisions.d.ts +1 -1
- package/dist/tools/shared.d.ts +2 -2
- package/package.json +6 -4
- package/src/corpus/ordering.ts +13 -38
- package/src/corpus/projection.ts +8 -27
- package/src/index.ts +35 -20
- package/src/main-module.ts +20 -1
- package/src/server.ts +38 -5
- package/src/tools/get-decision-context.ts +7 -5
- package/src/tools/get-decision.ts +1 -1
- package/src/tools/list-superseded.ts +1 -1
- package/src/tools/search-decisions.ts +1 -1
- package/src/tools/shared.ts +10 -10
package/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,
|
|
20
|
-
*
|
|
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 {
|
|
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/
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
7
7
|
|
|
8
8
|
// src/corpus/ordering.ts
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1054
|
-
server = nextServer;
|
|
1055
|
-
await nextServer.connect(new StdioServerTransport);
|
|
1036
|
+
connection = serveStdio(() => buildRegisteredServer(config), { onerror: onError });
|
|
1056
1037
|
} catch (error) {
|
|
1057
|
-
|
|
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 =
|
|
1080
|
-
|
|
1052
|
+
const current = connection;
|
|
1053
|
+
connection = undefined;
|
|
1081
1054
|
if (current)
|
|
1082
1055
|
await current.close();
|
|
1083
1056
|
})();
|
package/dist/main-module.d.ts
CHANGED
|
@@ -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/
|
|
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.
|
|
13
|
+
readonly version: "0.3.0";
|
|
14
14
|
};
|
|
15
|
-
/**
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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;
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -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/
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
48
|
-
"@modelcontextprotocol/
|
|
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": [
|
package/src/corpus/ordering.ts
CHANGED
|
@@ -1,41 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @adrkit/mcp —
|
|
3
|
-
*
|
|
2
|
+
* @adrkit/mcp — re-export shim.
|
|
3
|
+
*
|
|
4
|
+
* The comparator and canonical orderings were promoted to `@adrkit/core`
|
|
5
|
+
* (`packages/core/src/ordering/index.ts`). This module preserves every existing
|
|
6
|
+
* `../corpus/ordering` import site while the single implementation now lives in core.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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';
|
package/src/corpus/projection.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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,
|
|
30
|
-
*
|
|
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
|
|
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
|
-
|
|
61
|
-
server = nextServer;
|
|
62
|
-
await nextServer.connect(new StdioServerTransport());
|
|
85
|
+
connection = serveStdio(() => buildRegisteredServer(config), { onerror: onError });
|
|
63
86
|
} catch (error) {
|
|
64
|
-
|
|
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 =
|
|
86
|
-
|
|
100
|
+
const current = connection;
|
|
101
|
+
connection = undefined;
|
|
87
102
|
if (current) await current.close();
|
|
88
103
|
})();
|
|
89
104
|
return closePromise;
|
package/src/main-module.ts
CHANGED
|
@@ -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));
|