@descent-vtt/spec-guard 0.6.0 → 0.8.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/CHANGELOG.md +132 -8
- package/README.md +191 -9
- package/dist/cli.d.ts +17 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +127 -7
- package/dist/cli.js.map +1 -1
- package/dist/glob.d.ts +25 -2
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +26 -9
- package/dist/glob.js.map +1 -1
- package/dist/index.d.ts +17 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +117 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +545 -0
- package/dist/mcp.js.map +1 -0
- package/dist/parser.d.ts +25 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +71 -15
- package/dist/parser.js.map +1 -1
- package/dist/query.d.ts +108 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +211 -0
- package/dist/query.js.map +1 -0
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +13 -4
- package/dist/reporter.js.map +1 -1
- package/dist/rules.d.ts +119 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +197 -0
- package/dist/rules.js.map +1 -0
- package/dist/runner.d.ts +19 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +289 -97
- package/dist/runner.js.map +1 -1
- package/dist/specs.d.ts +44 -0
- package/dist/specs.d.ts.map +1 -0
- package/dist/specs.js +62 -0
- package/dist/specs.js.map +1 -0
- package/dist/structure.d.ts +104 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +253 -0
- package/dist/structure.js.map +1 -0
- package/dist/types.d.ts +33 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spec-guard mcp`: the rules, served to an agent over the Model Context
|
|
3
|
+
* Protocol on stdio.
|
|
4
|
+
*
|
|
5
|
+
* No SDK. MCP over stdio is JSON-RPC 2.0, one message per line, and what a
|
|
6
|
+
* server has to get right fits in this file: framing, the two eras of the
|
|
7
|
+
* protocol, and four methods' worth of results. The official SDK brings a
|
|
8
|
+
* schema library and a dependency tree to do the same, and spec-guard's first
|
|
9
|
+
* invariant is that it has no runtime dependencies at all. ADR-0012 has the
|
|
10
|
+
* rest, including what this file deliberately does not implement.
|
|
11
|
+
*
|
|
12
|
+
* Two eras, served side by side. Revisions up to 2025-11-25 open with an
|
|
13
|
+
* `initialize` handshake; 2026-07-28 has none, and every request carries its
|
|
14
|
+
* protocol version in `_meta`. Clients of both are in use, so each request is
|
|
15
|
+
* classified on its own - by the rule the TypeScript SDK's server applies, not
|
|
16
|
+
* by one invented here: a request that claims a version in `_meta` is modern
|
|
17
|
+
* and its claim is validated; anything else, `initialize` included, is legacy.
|
|
18
|
+
*/
|
|
19
|
+
import type { Readable } from 'node:stream';
|
|
20
|
+
import { type RunOptions } from './runner.js';
|
|
21
|
+
/** Revisions that carry their version on every request. Newest first. */
|
|
22
|
+
export declare const MODERN_PROTOCOL_VERSIONS: readonly string[];
|
|
23
|
+
/** Revisions negotiated by `initialize`, newest first - the SDK's own list. */
|
|
24
|
+
export declare const LEGACY_PROTOCOL_VERSIONS: readonly string[];
|
|
25
|
+
export declare const PROTOCOL_VERSION_KEY = "io.modelcontextprotocol/protocolVersion";
|
|
26
|
+
export declare const CLIENT_CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
|
|
27
|
+
export declare const CLIENT_INFO_KEY = "io.modelcontextprotocol/clientInfo";
|
|
28
|
+
export declare const SERVER_INFO_KEY = "io.modelcontextprotocol/serverInfo";
|
|
29
|
+
export declare const PARSE_ERROR = -32700;
|
|
30
|
+
export declare const INVALID_REQUEST = -32600;
|
|
31
|
+
export declare const METHOD_NOT_FOUND = -32601;
|
|
32
|
+
export declare const INVALID_PARAMS = -32602;
|
|
33
|
+
export declare const INTERNAL_ERROR = -32603;
|
|
34
|
+
/** Resource not found before 2026-07-28, which forbids it in favour of -32602. */
|
|
35
|
+
export declare const LEGACY_RESOURCE_NOT_FOUND = -32002;
|
|
36
|
+
export declare const UNSUPPORTED_PROTOCOL_VERSION = -32022;
|
|
37
|
+
export type RequestId = string | number;
|
|
38
|
+
export type Era = 'legacy' | 'modern';
|
|
39
|
+
export interface ErrorObject {
|
|
40
|
+
code: number;
|
|
41
|
+
message: string;
|
|
42
|
+
data?: unknown;
|
|
43
|
+
}
|
|
44
|
+
/** Everything this server ever writes. It never sends a request. */
|
|
45
|
+
export type OutgoingMessage = {
|
|
46
|
+
jsonrpc: '2.0';
|
|
47
|
+
id: RequestId;
|
|
48
|
+
result: Record<string, unknown>;
|
|
49
|
+
} | {
|
|
50
|
+
jsonrpc: '2.0';
|
|
51
|
+
id?: RequestId;
|
|
52
|
+
error: ErrorObject;
|
|
53
|
+
};
|
|
54
|
+
type JsonObject = Record<string, unknown>;
|
|
55
|
+
/**
|
|
56
|
+
* What is wrong with a request's `_meta`, once it claims a protocol version.
|
|
57
|
+
*
|
|
58
|
+
* Only what the server depends on or the revision requires: a version that is a
|
|
59
|
+
* string, capabilities that are an object, and client info shaped as client info
|
|
60
|
+
* when it is sent. A claim is never silently ignored - a malformed one is an
|
|
61
|
+
* error, not a reason to fall back to legacy handling.
|
|
62
|
+
*/
|
|
63
|
+
export declare function envelopeIssue(meta: JsonObject): string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* The era a request belongs to, or the error that answers it instead.
|
|
66
|
+
*
|
|
67
|
+
* `initialize` is the legacy handshake unless it carries a valid modern claim.
|
|
68
|
+
* Any other request is modern exactly when `_meta` holds the version key - its
|
|
69
|
+
* presence is the claim, whatever its value - and legacy otherwise.
|
|
70
|
+
*/
|
|
71
|
+
export declare function classifyRequest(method: string, params: JsonObject | undefined): Era;
|
|
72
|
+
/** The version `initialize` settles on: the one asked for, or the newest legacy one. */
|
|
73
|
+
export declare function negotiateLegacyVersion(requested: unknown): string;
|
|
74
|
+
export declare const SERVER_NAME = "spec-guard";
|
|
75
|
+
export declare const INSTRUCTIONS: string;
|
|
76
|
+
export declare const TOOLS: readonly JsonObject[];
|
|
77
|
+
export declare const RULES_URI = "spec://rules";
|
|
78
|
+
export declare const DOCUMENT_URI_PREFIX = "spec://doc/";
|
|
79
|
+
export declare const RESOURCE_TEMPLATES: readonly JsonObject[];
|
|
80
|
+
export declare function documentUri(relativeFile: string): string;
|
|
81
|
+
/** The spec path a document URI names, or null for anything else. */
|
|
82
|
+
export declare function documentPath(uri: string): string | null;
|
|
83
|
+
export interface McpServerOptions {
|
|
84
|
+
/** Absolute project root. */
|
|
85
|
+
root: string;
|
|
86
|
+
/** Spec globs, as on the command line. */
|
|
87
|
+
patterns: readonly string[];
|
|
88
|
+
/** Reported as the server's version. */
|
|
89
|
+
version: string;
|
|
90
|
+
/**
|
|
91
|
+
* Run settings, as the CLI flags set them. `ignoreStatus` also makes
|
|
92
|
+
* `get_architectural_rules` list rules from documents not in force by default.
|
|
93
|
+
*/
|
|
94
|
+
run?: Omit<RunOptions, 'patterns' | 'root' | 'select'>;
|
|
95
|
+
/** Reads a spec document. Injected so an unreadable one can be tested. */
|
|
96
|
+
readFile?: (file: string) => Promise<string>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Builds the function that answers one parsed JSON-RPC message.
|
|
100
|
+
*
|
|
101
|
+
* Returns the message to send back, or null when nothing is owed: a
|
|
102
|
+
* notification, or a response the client should not have sent.
|
|
103
|
+
*/
|
|
104
|
+
export declare function createMcpHandler(options: McpServerOptions): (message: unknown) => Promise<OutgoingMessage | null>;
|
|
105
|
+
/**
|
|
106
|
+
* Serves a handler over newline-delimited JSON-RPC until the input ends.
|
|
107
|
+
*
|
|
108
|
+
* The whole stdio binding. Messages are split on `\n` with a trailing `\r`
|
|
109
|
+
* dropped, blank lines are skipped, and every response is written as a single
|
|
110
|
+
* line - `JSON.stringify` never emits a raw newline, which is what the binding
|
|
111
|
+
* requires. Requests are answered concurrently and in whatever order they
|
|
112
|
+
* finish. A request the client cancels gets no response at all. When the input
|
|
113
|
+
* closes, requests still running are allowed to finish and then this resolves.
|
|
114
|
+
*/
|
|
115
|
+
export declare function serveStdio(input: Readable, write: (line: string) => void, handle: (message: unknown) => Promise<OutgoingMessage | null>): Promise<void>;
|
|
116
|
+
export {};
|
|
117
|
+
//# sourceMappingURL=mcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spec-guard mcp`: the rules, served to an agent over the Model Context
|
|
3
|
+
* Protocol on stdio.
|
|
4
|
+
*
|
|
5
|
+
* No SDK. MCP over stdio is JSON-RPC 2.0, one message per line, and what a
|
|
6
|
+
* server has to get right fits in this file: framing, the two eras of the
|
|
7
|
+
* protocol, and four methods' worth of results. The official SDK brings a
|
|
8
|
+
* schema library and a dependency tree to do the same, and spec-guard's first
|
|
9
|
+
* invariant is that it has no runtime dependencies at all. ADR-0012 has the
|
|
10
|
+
* rest, including what this file deliberately does not implement.
|
|
11
|
+
*
|
|
12
|
+
* Two eras, served side by side. Revisions up to 2025-11-25 open with an
|
|
13
|
+
* `initialize` handshake; 2026-07-28 has none, and every request carries its
|
|
14
|
+
* protocol version in `_meta`. Clients of both are in use, so each request is
|
|
15
|
+
* classified on its own - by the rule the TypeScript SDK's server applies, not
|
|
16
|
+
* by one invented here: a request that claims a version in `_meta` is modern
|
|
17
|
+
* and its claim is validated; anything else, `initialize` included, is legacy.
|
|
18
|
+
*/
|
|
19
|
+
import { promises as fs } from 'node:fs';
|
|
20
|
+
import { toPosix } from './glob.js';
|
|
21
|
+
import { formatReport } from './reporter.js';
|
|
22
|
+
import { elapsed, runSpecGuard } from './runner.js';
|
|
23
|
+
import { answerQuery, formatQuery, inQueriedPaths, loadRuleSet, QueryPathError, resolveQueryPath, viewDocument, } from './query.js';
|
|
24
|
+
import { governs, viewRule } from './rules.js';
|
|
25
|
+
/* ----------------------------------------------------------------- protocol */
|
|
26
|
+
/** Revisions that carry their version on every request. Newest first. */
|
|
27
|
+
export const MODERN_PROTOCOL_VERSIONS = ['2026-07-28'];
|
|
28
|
+
/** Revisions negotiated by `initialize`, newest first - the SDK's own list. */
|
|
29
|
+
export const LEGACY_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07'];
|
|
30
|
+
export const PROTOCOL_VERSION_KEY = 'io.modelcontextprotocol/protocolVersion';
|
|
31
|
+
export const CLIENT_CAPABILITIES_KEY = 'io.modelcontextprotocol/clientCapabilities';
|
|
32
|
+
export const CLIENT_INFO_KEY = 'io.modelcontextprotocol/clientInfo';
|
|
33
|
+
export const SERVER_INFO_KEY = 'io.modelcontextprotocol/serverInfo';
|
|
34
|
+
export const PARSE_ERROR = -32700;
|
|
35
|
+
export const INVALID_REQUEST = -32600;
|
|
36
|
+
export const METHOD_NOT_FOUND = -32601;
|
|
37
|
+
export const INVALID_PARAMS = -32602;
|
|
38
|
+
export const INTERNAL_ERROR = -32603;
|
|
39
|
+
/** Resource not found before 2026-07-28, which forbids it in favour of -32602. */
|
|
40
|
+
export const LEGACY_RESOURCE_NOT_FOUND = -32002;
|
|
41
|
+
export const UNSUPPORTED_PROTOCOL_VERSION = -32022;
|
|
42
|
+
class ProtocolError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
data;
|
|
45
|
+
constructor(code, message, data) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.data = data;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function isObject(value) {
|
|
52
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
53
|
+
}
|
|
54
|
+
/** JSON-RPC allows a null id; MCP does not, and requires an integer if a number. */
|
|
55
|
+
function isRequestId(value) {
|
|
56
|
+
return typeof value === 'string' || Number.isInteger(value);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* What is wrong with a request's `_meta`, once it claims a protocol version.
|
|
60
|
+
*
|
|
61
|
+
* Only what the server depends on or the revision requires: a version that is a
|
|
62
|
+
* string, capabilities that are an object, and client info shaped as client info
|
|
63
|
+
* when it is sent. A claim is never silently ignored - a malformed one is an
|
|
64
|
+
* error, not a reason to fall back to legacy handling.
|
|
65
|
+
*/
|
|
66
|
+
export function envelopeIssue(meta) {
|
|
67
|
+
if (!(CLIENT_CAPABILITIES_KEY in meta))
|
|
68
|
+
return `${CLIENT_CAPABILITIES_KEY}: missing`;
|
|
69
|
+
if (typeof meta[PROTOCOL_VERSION_KEY] !== 'string')
|
|
70
|
+
return `${PROTOCOL_VERSION_KEY}: expected a string`;
|
|
71
|
+
if (!isObject(meta[CLIENT_CAPABILITIES_KEY]))
|
|
72
|
+
return `${CLIENT_CAPABILITIES_KEY}: expected an object`;
|
|
73
|
+
const info = meta[CLIENT_INFO_KEY];
|
|
74
|
+
if (info !== undefined && !(isObject(info) && typeof info['name'] === 'string' && typeof info['version'] === 'string')) {
|
|
75
|
+
return `${CLIENT_INFO_KEY}: expected an object with a string name and version`;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The era a request belongs to, or the error that answers it instead.
|
|
81
|
+
*
|
|
82
|
+
* `initialize` is the legacy handshake unless it carries a valid modern claim.
|
|
83
|
+
* Any other request is modern exactly when `_meta` holds the version key - its
|
|
84
|
+
* presence is the claim, whatever its value - and legacy otherwise.
|
|
85
|
+
*/
|
|
86
|
+
export function classifyRequest(method, params) {
|
|
87
|
+
const meta = params?.['_meta'];
|
|
88
|
+
const claimed = isObject(meta) && PROTOCOL_VERSION_KEY in meta;
|
|
89
|
+
if (!claimed)
|
|
90
|
+
return 'legacy';
|
|
91
|
+
const issue = envelopeIssue(meta);
|
|
92
|
+
const version = meta[PROTOCOL_VERSION_KEY];
|
|
93
|
+
const supported = issue === undefined && MODERN_PROTOCOL_VERSIONS.includes(version);
|
|
94
|
+
if (method === 'initialize' && !supported)
|
|
95
|
+
return 'legacy';
|
|
96
|
+
if (issue !== undefined)
|
|
97
|
+
throw new ProtocolError(INVALID_PARAMS, `Invalid _meta envelope: ${issue}`);
|
|
98
|
+
if (!supported) {
|
|
99
|
+
throw new ProtocolError(UNSUPPORTED_PROTOCOL_VERSION, 'Unsupported protocol version', {
|
|
100
|
+
supported: [...MODERN_PROTOCOL_VERSIONS],
|
|
101
|
+
requested: version,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return 'modern';
|
|
105
|
+
}
|
|
106
|
+
/** The version `initialize` settles on: the one asked for, or the newest legacy one. */
|
|
107
|
+
export function negotiateLegacyVersion(requested) {
|
|
108
|
+
// No typeof check: a list of strings includes nothing that is not one.
|
|
109
|
+
return LEGACY_PROTOCOL_VERSIONS.includes(requested) ? requested : LEGACY_PROTOCOL_VERSIONS[0];
|
|
110
|
+
}
|
|
111
|
+
/* -------------------------------------------------------------- the surface */
|
|
112
|
+
export const SERVER_NAME = 'spec-guard';
|
|
113
|
+
export const INSTRUCTIONS = 'spec-guard enforces the architecture decisions written in this project\'s Markdown specs and ADRs. ' +
|
|
114
|
+
'Before creating or changing a file, call get_architectural_rules with its path to learn the rules in force there: ' +
|
|
115
|
+
'imports it must not make, the layer it belongs to, what it must be named and the files it needs beside it, text it must not contain. ' +
|
|
116
|
+
'After changing files, call check_architecture with their paths to find violations before CI does. ' +
|
|
117
|
+
'Rules in draft, proposed, rejected, deprecated or superseded documents are not in force and are only counted.';
|
|
118
|
+
const READ_ONLY = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
|
|
119
|
+
export const TOOLS = [
|
|
120
|
+
{
|
|
121
|
+
name: 'get_architectural_rules',
|
|
122
|
+
title: 'Architectural rules for a path',
|
|
123
|
+
description: 'Lists the architecture rules in force for a file or directory, grouped by the ADR or spec that states them: ' +
|
|
124
|
+
'modules it must not import, the layer it belongs to and the layers it must not depend on, import cycles, ' +
|
|
125
|
+
'what it must be named and the partner files it needs, ' +
|
|
126
|
+
'and text or symbols it must not contain, each with the reason the document gives. ' +
|
|
127
|
+
'Answers from the specs without reading the codebase, so it works for a file that does not exist yet. ' +
|
|
128
|
+
'Rules in documents that are not in force are counted and named but not listed unless include_inactive is true.',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
path: {
|
|
133
|
+
type: 'string',
|
|
134
|
+
description: 'A file or directory, relative to the project root or absolute inside it.',
|
|
135
|
+
},
|
|
136
|
+
include_inactive: {
|
|
137
|
+
type: 'boolean',
|
|
138
|
+
description: 'Also list rules from draft, proposed, rejected, deprecated and superseded documents.',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
required: ['path'],
|
|
142
|
+
additionalProperties: false,
|
|
143
|
+
},
|
|
144
|
+
annotations: READ_ONLY,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'check_architecture',
|
|
148
|
+
title: 'Check the architecture rules',
|
|
149
|
+
description: 'Runs the architecture rules in force against the files on disk and reports every violation, as CI would. ' +
|
|
150
|
+
'Given paths, runs only the rules that govern them - each over its whole scope, so a count or a cycle is judged ' +
|
|
151
|
+
'exactly as CI judges it - and marks which violations lie in those paths. Save your edits before calling.',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
paths: {
|
|
156
|
+
type: 'array',
|
|
157
|
+
items: { type: 'string' },
|
|
158
|
+
description: 'Files or directories to check the rules of, relative to the project root. Omit to run every rule.',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
},
|
|
163
|
+
annotations: READ_ONLY,
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
export const RULES_URI = 'spec://rules';
|
|
167
|
+
export const DOCUMENT_URI_PREFIX = 'spec://doc/';
|
|
168
|
+
export const RESOURCE_TEMPLATES = [
|
|
169
|
+
{
|
|
170
|
+
uriTemplate: `${DOCUMENT_URI_PREFIX}{+path}`,
|
|
171
|
+
name: 'spec-document',
|
|
172
|
+
title: 'Spec document',
|
|
173
|
+
description: 'Any spec document by its path relative to the project root, including documents that are not in force.',
|
|
174
|
+
mimeType: 'text/markdown',
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
export function documentUri(relativeFile) {
|
|
178
|
+
return `${DOCUMENT_URI_PREFIX}${relativeFile.split('/').map(encodeURIComponent).join('/')}`;
|
|
179
|
+
}
|
|
180
|
+
/** The spec path a document URI names, or null for anything else. */
|
|
181
|
+
export function documentPath(uri) {
|
|
182
|
+
if (!uri.startsWith(DOCUMENT_URI_PREFIX))
|
|
183
|
+
return null;
|
|
184
|
+
try {
|
|
185
|
+
return uri.slice(DOCUMENT_URI_PREFIX.length).split('/').map(decodeURIComponent).join('/');
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Malformed percent-encoding names no document.
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function toolError(text) {
|
|
193
|
+
return { text, isError: true };
|
|
194
|
+
}
|
|
195
|
+
/** Arguments a tool does not declare, which the model should hear about. */
|
|
196
|
+
function unknownArguments(args, allowed) {
|
|
197
|
+
const unknown = Object.keys(args).filter((name) => !allowed.includes(name));
|
|
198
|
+
return unknown.length === 0
|
|
199
|
+
? undefined
|
|
200
|
+
: toolError(`Unknown argument${unknown.length === 1 ? '' : 's'} ${unknown.map((name) => `"${name}"`).join(', ')}; this tool takes ${allowed.join(' and ')}.`);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Builds the function that answers one parsed JSON-RPC message.
|
|
204
|
+
*
|
|
205
|
+
* Returns the message to send back, or null when nothing is owed: a
|
|
206
|
+
* notification, or a response the client should not have sent.
|
|
207
|
+
*/
|
|
208
|
+
export function createMcpHandler(options) {
|
|
209
|
+
const serverInfo = { name: SERVER_NAME, version: options.version };
|
|
210
|
+
const capabilities = { tools: {}, resources: {} };
|
|
211
|
+
const readFile = options.readFile ?? ((file) => fs.readFile(file, 'utf8'));
|
|
212
|
+
const ruleSetOptions = {
|
|
213
|
+
patterns: options.patterns,
|
|
214
|
+
root: options.root,
|
|
215
|
+
// No defaults of their own: loadRuleSet has them, and a second copy here
|
|
216
|
+
// could never disagree with it in a way anything could see.
|
|
217
|
+
includeSpecs: options.run?.includeSpecs,
|
|
218
|
+
defaultSkips: options.run?.defaultSkips,
|
|
219
|
+
};
|
|
220
|
+
const noSpecs = () => toolError(`No spec files matched ${options.patterns.map((pattern) => `"${pattern}"`).join(', ')} under ${options.root}. ` +
|
|
221
|
+
'Start the server with --spec <glob> or --root <dir> pointing at the project.');
|
|
222
|
+
async function getRules(args) {
|
|
223
|
+
const unknown = unknownArguments(args, ['path', 'include_inactive']);
|
|
224
|
+
if (unknown)
|
|
225
|
+
return unknown;
|
|
226
|
+
if (typeof args['path'] !== 'string')
|
|
227
|
+
return toolError('"path" is required and must be a string.');
|
|
228
|
+
const includeInactive = args['include_inactive'] ?? options.run?.ignoreStatus ?? false;
|
|
229
|
+
if (typeof includeInactive !== 'boolean')
|
|
230
|
+
return toolError('"include_inactive" must be true or false.');
|
|
231
|
+
const startedAt = performance.now();
|
|
232
|
+
const query = await resolveQueryPath(args['path'], options.root);
|
|
233
|
+
const ruleSet = await loadRuleSet(ruleSetOptions);
|
|
234
|
+
if (ruleSet.specFiles.length === 0)
|
|
235
|
+
return noSpecs();
|
|
236
|
+
const report = { ...answerQuery(ruleSet, [query], includeInactive), durationMs: elapsed(startedAt) };
|
|
237
|
+
return { text: formatQuery(report), structured: { ...report } };
|
|
238
|
+
}
|
|
239
|
+
async function checkArchitecture(args) {
|
|
240
|
+
const unknown = unknownArguments(args, ['paths']);
|
|
241
|
+
if (unknown)
|
|
242
|
+
return unknown;
|
|
243
|
+
const rawPaths = args['paths'];
|
|
244
|
+
if (rawPaths !== undefined && !(Array.isArray(rawPaths) && rawPaths.every((entry) => typeof entry === 'string'))) {
|
|
245
|
+
return toolError('"paths" must be an array of strings.');
|
|
246
|
+
}
|
|
247
|
+
const paths = rawPaths === undefined ? undefined : await Promise.all(rawPaths.map((entry) => resolveQueryPath(entry, options.root)));
|
|
248
|
+
let inForce = 0;
|
|
249
|
+
const report = await runSpecGuard({
|
|
250
|
+
...options.run,
|
|
251
|
+
patterns: options.patterns,
|
|
252
|
+
root: options.root,
|
|
253
|
+
select: (assertion) => {
|
|
254
|
+
inForce += 1;
|
|
255
|
+
return paths === undefined || paths.some((query) => governs(assertion, query));
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
if (report.summary.specs === 0)
|
|
259
|
+
return noSpecs();
|
|
260
|
+
const scope = paths === undefined
|
|
261
|
+
? `all ${inForce} rules in force`
|
|
262
|
+
: `${report.summary.total} of ${inForce} rules in force - the ones that govern ${paths.map((query) => query.path).join(', ')}`;
|
|
263
|
+
const structured = {
|
|
264
|
+
ok: report.ok,
|
|
265
|
+
engine: report.engine,
|
|
266
|
+
paths: paths?.map((query) => query.path) ?? null,
|
|
267
|
+
rules: { inForce, checked: report.summary.total, passed: report.summary.passed, failed: report.summary.failed },
|
|
268
|
+
failures: report.results
|
|
269
|
+
.filter((result) => !result.ok)
|
|
270
|
+
.map((result) => ({
|
|
271
|
+
document: result.location.relativeFile,
|
|
272
|
+
line: result.location.line,
|
|
273
|
+
kind: result.kind,
|
|
274
|
+
description: result.description,
|
|
275
|
+
reason: result.reason ?? null,
|
|
276
|
+
message: result.message,
|
|
277
|
+
matches: result.matches.map((match) => ({
|
|
278
|
+
...match,
|
|
279
|
+
inPaths: paths === undefined || inQueriedPaths(match.file, paths),
|
|
280
|
+
})),
|
|
281
|
+
warnings: result.warnings,
|
|
282
|
+
})),
|
|
283
|
+
errors: report.errors.map((error) => ({
|
|
284
|
+
file: error.location.relativeFile,
|
|
285
|
+
line: error.location.line,
|
|
286
|
+
message: error.message,
|
|
287
|
+
})),
|
|
288
|
+
inactiveSpecs: report.inactiveSpecs,
|
|
289
|
+
warnings: report.warnings,
|
|
290
|
+
durationMs: Math.round(report.durationMs * 1000) / 1000,
|
|
291
|
+
};
|
|
292
|
+
const text = `Checked ${scope}.\n\n${formatReport(report, { color: false, verbose: false, ascii: true }, options.run?.maxSnippets)}`;
|
|
293
|
+
return { text, structured };
|
|
294
|
+
}
|
|
295
|
+
async function callTool(params) {
|
|
296
|
+
const name = params['name'];
|
|
297
|
+
if (typeof name !== 'string')
|
|
298
|
+
throw new ProtocolError(INVALID_PARAMS, 'tools/call needs the name of a tool.');
|
|
299
|
+
const tool = name === 'get_architectural_rules' ? getRules : name === 'check_architecture' ? checkArchitecture : undefined;
|
|
300
|
+
if (tool === undefined)
|
|
301
|
+
throw new ProtocolError(INVALID_PARAMS, `Unknown tool: ${name}`);
|
|
302
|
+
const args = params['arguments'] ?? {};
|
|
303
|
+
if (!isObject(args))
|
|
304
|
+
throw new ProtocolError(INVALID_PARAMS, 'Tool arguments must be an object.');
|
|
305
|
+
let outcome;
|
|
306
|
+
try {
|
|
307
|
+
outcome = await tool(args);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
// A path outside the root is the model's to fix; anything else is still
|
|
311
|
+
// better reported to the model than turned into a protocol failure it
|
|
312
|
+
// cannot read.
|
|
313
|
+
outcome = toolError(error instanceof QueryPathError ? error.message : `spec-guard failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: 'text', text: outcome.text }],
|
|
317
|
+
...(outcome.structured === undefined ? {} : { structuredContent: outcome.structured }),
|
|
318
|
+
...(outcome.isError ? { isError: true } : {}),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
async function listResources() {
|
|
322
|
+
const ruleSet = await loadRuleSet(ruleSetOptions);
|
|
323
|
+
return {
|
|
324
|
+
resources: [
|
|
325
|
+
{
|
|
326
|
+
uri: RULES_URI,
|
|
327
|
+
name: 'rules',
|
|
328
|
+
title: 'Rules in force',
|
|
329
|
+
description: 'Every rule in force, with the document that states it and its scope, as JSON.',
|
|
330
|
+
mimeType: 'application/json',
|
|
331
|
+
},
|
|
332
|
+
// Documents not in force are readable through the template and left out
|
|
333
|
+
// of the list, which a client may put in front of a model wholesale.
|
|
334
|
+
...ruleSet.documents
|
|
335
|
+
.filter((document) => document.inForce)
|
|
336
|
+
.map((document) => ({
|
|
337
|
+
uri: documentUri(document.relativeFile),
|
|
338
|
+
name: document.relativeFile,
|
|
339
|
+
title: document.title ?? document.relativeFile,
|
|
340
|
+
mimeType: 'text/markdown',
|
|
341
|
+
})),
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function rulesResource(ruleSet) {
|
|
346
|
+
return {
|
|
347
|
+
root: toPosix(options.root),
|
|
348
|
+
specFiles: ruleSet.specFiles,
|
|
349
|
+
rules: ruleSet.rules
|
|
350
|
+
.filter(({ document }) => document.inForce)
|
|
351
|
+
.map(({ assertion, document }) => viewRule(assertion, document)),
|
|
352
|
+
notInForce: ruleSet.documents
|
|
353
|
+
.filter((document) => !document.inForce)
|
|
354
|
+
.map((document) => ({ ...viewDocument(document), rules: document.directives.length })),
|
|
355
|
+
errors: ruleSet.errors.map((error) => ({
|
|
356
|
+
file: error.location.relativeFile,
|
|
357
|
+
line: error.location.line,
|
|
358
|
+
message: error.message,
|
|
359
|
+
})),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
async function readResource(params, era) {
|
|
363
|
+
const uri = params['uri'];
|
|
364
|
+
if (typeof uri !== 'string')
|
|
365
|
+
throw new ProtocolError(INVALID_PARAMS, 'resources/read needs a uri.');
|
|
366
|
+
const ruleSet = await loadRuleSet(ruleSetOptions);
|
|
367
|
+
if (uri === RULES_URI) {
|
|
368
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(rulesResource(ruleSet), null, 2) }] };
|
|
369
|
+
}
|
|
370
|
+
// Membership, never a path join: the only files this reads are the ones the
|
|
371
|
+
// spec patterns matched, so no URI can reach anything else on disk.
|
|
372
|
+
const wanted = documentPath(uri);
|
|
373
|
+
const document = ruleSet.documents.find((candidate) => candidate.relativeFile === wanted);
|
|
374
|
+
const text = document === undefined ? undefined : await readFile(document.file).catch(() => undefined);
|
|
375
|
+
if (text === undefined) {
|
|
376
|
+
throw new ProtocolError(era === 'modern' ? INVALID_PARAMS : LEGACY_RESOURCE_NOT_FOUND, 'Resource not found', { uri });
|
|
377
|
+
}
|
|
378
|
+
return { contents: [{ uri, mimeType: 'text/markdown', text }] };
|
|
379
|
+
}
|
|
380
|
+
/** A list request's cursor. This server never issues one, so any is invalid. */
|
|
381
|
+
function refuseCursor(params) {
|
|
382
|
+
if (params?.['cursor'] !== undefined)
|
|
383
|
+
throw new ProtocolError(INVALID_PARAMS, 'Invalid cursor: this server does not paginate.');
|
|
384
|
+
}
|
|
385
|
+
async function dispatch(method, params, era) {
|
|
386
|
+
const given = params ?? {};
|
|
387
|
+
if (era === 'legacy') {
|
|
388
|
+
if (method === 'initialize') {
|
|
389
|
+
return {
|
|
390
|
+
protocolVersion: negotiateLegacyVersion(given['protocolVersion']),
|
|
391
|
+
capabilities,
|
|
392
|
+
serverInfo,
|
|
393
|
+
instructions: INSTRUCTIONS,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (method === 'ping')
|
|
397
|
+
return {};
|
|
398
|
+
}
|
|
399
|
+
else if (method === 'server/discover') {
|
|
400
|
+
return { supportedVersions: [...MODERN_PROTOCOL_VERSIONS], capabilities, instructions: INSTRUCTIONS };
|
|
401
|
+
}
|
|
402
|
+
switch (method) {
|
|
403
|
+
case 'tools/list':
|
|
404
|
+
refuseCursor(params);
|
|
405
|
+
return { tools: TOOLS };
|
|
406
|
+
case 'tools/call':
|
|
407
|
+
return callTool(given);
|
|
408
|
+
case 'resources/list':
|
|
409
|
+
refuseCursor(params);
|
|
410
|
+
return listResources();
|
|
411
|
+
case 'resources/templates/list':
|
|
412
|
+
refuseCursor(params);
|
|
413
|
+
return { resourceTemplates: RESOURCE_TEMPLATES };
|
|
414
|
+
case 'resources/read':
|
|
415
|
+
return readResource(given, era);
|
|
416
|
+
default:
|
|
417
|
+
throw new ProtocolError(METHOD_NOT_FOUND, 'Method not found');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/** The 2026-07-28 shape: a result type, caching hints where required, and identity. */
|
|
421
|
+
function modernResult(method, result) {
|
|
422
|
+
// Every cacheable result is marked stale at once and private. The rules are
|
|
423
|
+
// read fresh on each request, and a client serving an ADR edited a minute ago
|
|
424
|
+
// from cache would be doing the one thing this tool exists to stop.
|
|
425
|
+
const cacheable = method !== 'tools/call';
|
|
426
|
+
return {
|
|
427
|
+
resultType: 'complete',
|
|
428
|
+
...result,
|
|
429
|
+
...(cacheable ? { ttlMs: 0, cacheScope: 'private' } : {}),
|
|
430
|
+
_meta: { [SERVER_INFO_KEY]: serverInfo },
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return async (message) => {
|
|
434
|
+
if (!isObject(message)) {
|
|
435
|
+
return {
|
|
436
|
+
jsonrpc: '2.0',
|
|
437
|
+
error: {
|
|
438
|
+
code: INVALID_REQUEST,
|
|
439
|
+
message: Array.isArray(message) ? 'Invalid Request: batches are not supported' : 'Invalid Request',
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const { id, method, params } = message;
|
|
444
|
+
if (message['jsonrpc'] === '2.0' && method === undefined && isRequestId(id) && ('result' in message || 'error' in message)) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
if (message['jsonrpc'] !== '2.0' || typeof method !== 'string' || (id !== undefined && !isRequestId(id))) {
|
|
448
|
+
return { jsonrpc: '2.0', ...(isRequestId(id) ? { id } : {}), error: { code: INVALID_REQUEST, message: 'Invalid Request' } };
|
|
449
|
+
}
|
|
450
|
+
if (id === undefined)
|
|
451
|
+
return null;
|
|
452
|
+
try {
|
|
453
|
+
if (params !== undefined && !isObject(params))
|
|
454
|
+
throw new ProtocolError(INVALID_PARAMS, 'params must be an object.');
|
|
455
|
+
const era = classifyRequest(method, params);
|
|
456
|
+
const result = await dispatch(method, params, era);
|
|
457
|
+
return { jsonrpc: '2.0', id, result: era === 'modern' ? modernResult(method, result) : result };
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
const failure = error instanceof ProtocolError
|
|
461
|
+
? { code: error.code, message: error.message, data: error.data }
|
|
462
|
+
: { code: INTERNAL_ERROR, message: `Internal error: ${error instanceof Error ? error.message : String(error)}` };
|
|
463
|
+
return { jsonrpc: '2.0', id, error: failure };
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/* -------------------------------------------------------------------- stdio */
|
|
468
|
+
/** The key a request is tracked under: `1` and `"1"` are different ids. */
|
|
469
|
+
function trackingKey(id) {
|
|
470
|
+
return `${typeof id}:${id}`;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Serves a handler over newline-delimited JSON-RPC until the input ends.
|
|
474
|
+
*
|
|
475
|
+
* The whole stdio binding. Messages are split on `\n` with a trailing `\r`
|
|
476
|
+
* dropped, blank lines are skipped, and every response is written as a single
|
|
477
|
+
* line - `JSON.stringify` never emits a raw newline, which is what the binding
|
|
478
|
+
* requires. Requests are answered concurrently and in whatever order they
|
|
479
|
+
* finish. A request the client cancels gets no response at all. When the input
|
|
480
|
+
* closes, requests still running are allowed to finish and then this resolves.
|
|
481
|
+
*/
|
|
482
|
+
export function serveStdio(input, write, handle) {
|
|
483
|
+
return new Promise((resolve, reject) => {
|
|
484
|
+
// Decoded here rather than with setEncoding, which cannot be told apart
|
|
485
|
+
// from not setting one: Node reads an empty encoding name as UTF-8 too.
|
|
486
|
+
const decoder = new TextDecoder();
|
|
487
|
+
let buffer = '';
|
|
488
|
+
let running = 0;
|
|
489
|
+
let ended = false;
|
|
490
|
+
const inFlight = new Set();
|
|
491
|
+
const cancelled = new Set();
|
|
492
|
+
// Resolves once the input has ended and the last request has answered. A
|
|
493
|
+
// count rather than a set of promises, so that getting it wrong either hangs
|
|
494
|
+
// or ends early - both of which a test sees.
|
|
495
|
+
const settle = () => {
|
|
496
|
+
if (ended && running === 0)
|
|
497
|
+
resolve();
|
|
498
|
+
};
|
|
499
|
+
const receive = (line) => {
|
|
500
|
+
// A CRLF line keeps its \r: JSON allows it as whitespace, and a line of
|
|
501
|
+
// nothing else is blank.
|
|
502
|
+
if (line.trim() === '')
|
|
503
|
+
return;
|
|
504
|
+
let message;
|
|
505
|
+
try {
|
|
506
|
+
message = JSON.parse(line);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
write(JSON.stringify({ jsonrpc: '2.0', error: { code: PARSE_ERROR, message: 'Parse error' } }));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (isObject(message) && message['method'] === 'notifications/cancelled') {
|
|
513
|
+
const requestId = isObject(message['params']) ? message['params']['requestId'] : undefined;
|
|
514
|
+
if (isRequestId(requestId) && inFlight.has(trackingKey(requestId)))
|
|
515
|
+
cancelled.add(trackingKey(requestId));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const key = isObject(message) && typeof message['method'] === 'string' ? trackingKey(message['id']) : undefined;
|
|
519
|
+
inFlight.add(key);
|
|
520
|
+
running += 1;
|
|
521
|
+
void handle(message).then((response) => {
|
|
522
|
+
running -= 1;
|
|
523
|
+
inFlight.delete(key);
|
|
524
|
+
if (!cancelled.delete(key) && response !== null)
|
|
525
|
+
write(JSON.stringify(response));
|
|
526
|
+
settle();
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
input.on('data', (chunk) => {
|
|
530
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
531
|
+
for (let newline = buffer.indexOf('\n'); newline !== -1; newline = buffer.indexOf('\n')) {
|
|
532
|
+
const line = buffer.slice(0, newline);
|
|
533
|
+
buffer = buffer.slice(newline + 1);
|
|
534
|
+
receive(line);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
input.once('end', () => {
|
|
538
|
+
ended = true;
|
|
539
|
+
receive(buffer);
|
|
540
|
+
settle();
|
|
541
|
+
});
|
|
542
|
+
input.once('error', reject);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
//# sourceMappingURL=mcp.js.map
|