@descent-vtt/spec-guard 0.10.3 → 0.12.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 +410 -0
- package/README.md +385 -27
- package/dist/cites.d.ts +192 -0
- package/dist/cites.d.ts.map +1 -0
- package/dist/cites.js +599 -0
- package/dist/cites.js.map +1 -0
- package/dist/cli.d.ts +14 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +315 -22
- package/dist/cli.js.map +1 -1
- package/dist/comments.d.ts +51 -2
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +353 -57
- package/dist/comments.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -5
- package/dist/config.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +38 -14
- package/dist/engine.js.map +1 -1
- package/dist/glob.d.ts +128 -24
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +304 -61
- package/dist/glob.js.map +1 -1
- package/dist/impact.d.ts +179 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +360 -0
- package/dist/impact.js.map +1 -0
- package/dist/imports.d.ts.map +1 -1
- package/dist/imports.js +5 -58
- package/dist/imports.js.map +1 -1
- package/dist/index.d.ts +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +16 -75
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +198 -370
- package/dist/mcp.js.map +1 -1
- package/dist/overlay.d.ts +39 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +133 -0
- package/dist/overlay.js.map +1 -0
- package/dist/parser.d.ts +41 -24
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +217 -160
- package/dist/parser.js.map +1 -1
- package/dist/prove.d.ts +46 -0
- package/dist/prove.d.ts.map +1 -0
- package/dist/prove.js +549 -0
- package/dist/prove.js.map +1 -0
- package/dist/query.d.ts +11 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +9 -3
- package/dist/query.js.map +1 -1
- package/dist/reporter.d.ts +139 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +593 -21
- package/dist/reporter.js.map +1 -1
- package/dist/rules.js +3 -3
- package/dist/rules.js.map +1 -1
- package/dist/runner.d.ts +24 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +84 -21
- package/dist/runner.js.map +1 -1
- package/dist/specs.d.ts +5 -1
- package/dist/specs.d.ts.map +1 -1
- package/dist/specs.js +5 -1
- package/dist/specs.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +7 -6
- package/dist/structure.js.map +1 -1
- package/dist/types.d.ts +193 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vendor/spec-core/jsonrpc/index.d.ts +3 -0
- package/dist/vendor/spec-core/jsonrpc/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/index.js +2 -0
- package/dist/vendor/spec-core/jsonrpc/index.js.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.d.ts +158 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.d.ts.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js +353 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -0
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +104 -0
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts +46 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.js +199 -0
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
- package/dist/vendor/spec-core/markdown/index.d.ts +6 -0
- package/dist/vendor/spec-core/markdown/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/index.js +4 -0
- package/dist/vendor/spec-core/markdown/index.js.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts +40 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.js +28 -0
- package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
- package/dist/vendor/spec-core/markdown/links.d.ts +23 -0
- package/dist/vendor/spec-core/markdown/links.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/links.js +364 -0
- package/dist/vendor/spec-core/markdown/links.js.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts +23 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.js +127 -0
- package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts +37 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.js +439 -0
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts +76 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.js +181 -0
- package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts +19 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.js +86 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
- package/dist/vendor/spec-core/markdown/types.d.ts +288 -0
- package/dist/vendor/spec-core/markdown/types.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/types.js +9 -0
- package/dist/vendor/spec-core/markdown/types.js.map +1 -0
- package/dist/vendor/spec-core/path/index.d.ts +2 -0
- package/dist/vendor/spec-core/path/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/path/index.js +2 -0
- package/dist/vendor/spec-core/path/index.js.map +1 -0
- package/dist/vendor/spec-core/path/posix.d.ts +84 -0
- package/dist/vendor/spec-core/path/posix.d.ts.map +1 -0
- package/dist/vendor/spec-core/path/posix.js +150 -0
- package/dist/vendor/spec-core/path/posix.js.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts +130 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.js +340 -0
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts +59 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.js +116 -0
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts +122 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.js +481 -0
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
- package/dist/vendor/spec-core/pattern/index.d.ts +7 -0
- package/dist/vendor/spec-core/pattern/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/index.js +4 -0
- package/dist/vendor/spec-core/pattern/index.js.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts +68 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.js +813 -0
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
- package/dist/vendor/spec-core/text/index.d.ts +3 -0
- package/dist/vendor/spec-core/text/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/text/index.js +2 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -0
- package/dist/vendor/spec-core/text/lines.d.ts +73 -0
- package/dist/vendor/spec-core/text/lines.d.ts.map +1 -0
- package/dist/vendor/spec-core/text/lines.js +156 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -0
- package/package.json +8 -1
- package/src/vendor/spec-core/LICENSE +21 -0
- package/src/vendor/spec-core/README.md +8 -0
package/dist/mcp.js
CHANGED
|
@@ -2,167 +2,118 @@
|
|
|
2
2
|
* `spec-guard mcp`: the rules, served to an agent over the Model Context
|
|
3
3
|
* Protocol on stdio.
|
|
4
4
|
*
|
|
5
|
-
* No SDK.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
5
|
+
* No SDK. The protocol - framing, the two eras, classifying each request,
|
|
6
|
+
* dispatching it and shaping its result - is spec-core's `jsonrpc` module,
|
|
7
|
+
* copied into `src/vendor` and verified by hash (ADR-0015). It was written
|
|
8
|
+
* here first and moved there so that a second server in the family does not
|
|
9
|
+
* relearn it. What is spec-guard's, and stays here, is what the server says:
|
|
10
|
+
* its instructions, its three tools, and the rules and documents it serves as
|
|
11
|
+
* resources. ADR-0012 has the rest, including what this server deliberately
|
|
12
|
+
* does not implement; ADR-0018 has `get_dependents`.
|
|
18
13
|
*/
|
|
19
14
|
import { toPosix } from './glob.js';
|
|
15
|
+
import { formatImpact, ImpactError, impactDocument, impactOf } from './impact.js';
|
|
20
16
|
import { nodeIo, readText } from './io.js';
|
|
21
17
|
import { formatReport } from './reporter.js';
|
|
22
18
|
import { elapsed, runSpecGuard } from './runner.js';
|
|
23
19
|
import { answerQuery, formatQuery, inQueriedPaths, loadRuleSet, QueryPathError, resolveQueryPath, viewDocument, } from './query.js';
|
|
24
20
|
import { governs, viewRule } from './rules.js';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
}
|
|
21
|
+
import { createMcpServer, serveLines, toolError, unknownArguments, } from './vendor/spec-core/jsonrpc/index.js';
|
|
22
|
+
// The protocol's names, re-exported from where they now live, so that a
|
|
23
|
+
// caller of the API who imported them from here still finds them.
|
|
24
|
+
export { classifyRequest, envelopeIssue, negotiateLegacyVersion, CLIENT_CAPABILITIES_KEY, CLIENT_INFO_KEY, INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, LEGACY_PROTOCOL_VERSIONS, LEGACY_RESOURCE_NOT_FOUND, METHOD_NOT_FOUND, MODERN_PROTOCOL_VERSIONS, PARSE_ERROR, PROTOCOL_VERSION_KEY, SERVER_INFO_KEY, UNSUPPORTED_PROTOCOL_VERSION, } from './vendor/spec-core/jsonrpc/index.js';
|
|
111
25
|
/* -------------------------------------------------------------- the surface */
|
|
112
26
|
export const SERVER_NAME = 'spec-guard';
|
|
113
27
|
export const INSTRUCTIONS = 'spec-guard enforces the architecture decisions written in this project\'s Markdown specs and ADRs. ' +
|
|
114
28
|
'Before creating or changing a file, call get_architectural_rules with its path to learn the rules in force there: ' +
|
|
115
29
|
'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. ' +
|
|
30
|
+
'Before changing a file that other files import - renaming or removing an export, changing what a function takes or returns - ' +
|
|
31
|
+
'call get_dependents with its path to learn every file the change can reach and the rules in force over them. ' +
|
|
116
32
|
'After changing files, call check_architecture with their paths to find violations before CI does. ' +
|
|
117
|
-
'Rules in draft, proposed, rejected, deprecated or
|
|
33
|
+
'Rules in draft, proposed, rejected, deprecated, superseded or archived documents are not in force and are only counted.';
|
|
118
34
|
const READ_ONLY = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
description: 'Also list rules from draft, proposed, rejected, deprecated and superseded documents.',
|
|
139
|
-
},
|
|
35
|
+
const GET_RULES = {
|
|
36
|
+
name: 'get_architectural_rules',
|
|
37
|
+
title: 'Architectural rules for a path',
|
|
38
|
+
description: 'Lists the architecture rules in force for a file or directory, grouped by the ADR or spec that states them: ' +
|
|
39
|
+
'modules it must not import, the layer it belongs to and the layers it must not depend on, import cycles, ' +
|
|
40
|
+
'what it must be named and the partner files it needs, ' +
|
|
41
|
+
'and text or symbols it must not contain, each with the reason the document gives. ' +
|
|
42
|
+
'Answers from the specs without reading the codebase, so it works for a file that does not exist yet. ' +
|
|
43
|
+
'Rules in documents that are not in force are counted and named but not listed unless include_inactive is true.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
path: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'A file or directory, relative to the project root or absolute inside it.',
|
|
50
|
+
},
|
|
51
|
+
include_inactive: {
|
|
52
|
+
type: 'boolean',
|
|
53
|
+
description: 'Also list rules from draft, proposed, rejected, deprecated, superseded and archived documents.',
|
|
140
54
|
},
|
|
141
|
-
required: ['path'],
|
|
142
|
-
additionalProperties: false,
|
|
143
55
|
},
|
|
144
|
-
|
|
56
|
+
required: ['path'],
|
|
57
|
+
additionalProperties: false,
|
|
145
58
|
},
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
},
|
|
59
|
+
annotations: READ_ONLY,
|
|
60
|
+
};
|
|
61
|
+
const CHECK_ARCHITECTURE = {
|
|
62
|
+
name: 'check_architecture',
|
|
63
|
+
title: 'Check the architecture rules',
|
|
64
|
+
description: 'Runs the architecture rules in force against the files on disk and reports every violation, as CI would. ' +
|
|
65
|
+
'Given paths, runs only the rules that govern them - each over its whole scope, so a count or a cycle is judged ' +
|
|
66
|
+
'exactly as CI judges it - and marks which violations lie in those paths. Save your edits before calling.',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
paths: {
|
|
71
|
+
type: 'array',
|
|
72
|
+
items: { type: 'string' },
|
|
73
|
+
description: 'Files or directories to check the rules of, relative to the project root. Omit to run every rule.',
|
|
160
74
|
},
|
|
161
|
-
additionalProperties: false,
|
|
162
75
|
},
|
|
163
|
-
|
|
76
|
+
additionalProperties: false,
|
|
164
77
|
},
|
|
165
|
-
|
|
78
|
+
annotations: READ_ONLY,
|
|
79
|
+
};
|
|
80
|
+
const GET_DEPENDENTS = {
|
|
81
|
+
name: 'get_dependents',
|
|
82
|
+
title: 'Files that depend on a path',
|
|
83
|
+
description: 'Lists the files that import each given file or directory, directly or through other files, ' +
|
|
84
|
+
'each with how many imports away it is and the import that leads there, ' +
|
|
85
|
+
'and the architecture rules in force that govern the paths or any of those files. ' +
|
|
86
|
+
'Call it before changing a file that other files import - renaming or removing an export, changing a signature - ' +
|
|
87
|
+
'to see everything the change can reach. ' +
|
|
88
|
+
'Relative JavaScript, TypeScript and Python imports are followed; an import that cannot be resolved is listed, ' +
|
|
89
|
+
'and Go, Rust, C# and absolute Python imports, which name modules rather than files, are counted, never guessed, ' +
|
|
90
|
+
'so a short list is not mistaken for a complete one. Reads the codebase on disk, so every path must exist.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
paths: {
|
|
95
|
+
type: 'array',
|
|
96
|
+
items: { type: 'string' },
|
|
97
|
+
minItems: 1,
|
|
98
|
+
description: 'Files or directories that exist, relative to the project root or absolute inside it.',
|
|
99
|
+
},
|
|
100
|
+
depth: {
|
|
101
|
+
type: 'integer',
|
|
102
|
+
minimum: 1,
|
|
103
|
+
description: 'Follow dependents at most this many imports away. Omit to follow every one.',
|
|
104
|
+
},
|
|
105
|
+
include_inactive: {
|
|
106
|
+
type: 'boolean',
|
|
107
|
+
description: 'Also list rules from draft, proposed, rejected, deprecated, superseded and archived documents.',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
required: ['paths'],
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
},
|
|
113
|
+
annotations: READ_ONLY,
|
|
114
|
+
};
|
|
115
|
+
/** What `tools/list` answers, in the order it lists them. */
|
|
116
|
+
export const TOOLS = [GET_RULES, CHECK_ARCHITECTURE, GET_DEPENDENTS];
|
|
166
117
|
export const RULES_URI = 'spec://rules';
|
|
167
118
|
export const DOCUMENT_URI_PREFIX = 'spec://doc/';
|
|
168
119
|
export const RESOURCE_TEMPLATES = [
|
|
@@ -189,15 +140,24 @@ export function documentPath(uri) {
|
|
|
189
140
|
return null;
|
|
190
141
|
}
|
|
191
142
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
143
|
+
/**
|
|
144
|
+
* One of the server's tools.
|
|
145
|
+
*
|
|
146
|
+
* A path argument outside the root, or one that must exist and does not, is
|
|
147
|
+
* the model's to fix, so it hears the message alone. Anything else a tool
|
|
148
|
+
* throws is caught by the protocol layer and reported after the server's name,
|
|
149
|
+
* "spec-guard failed: ...", which is still better than a protocol failure the
|
|
150
|
+
* model cannot read.
|
|
151
|
+
*/
|
|
152
|
+
function tool(descriptor, run) {
|
|
153
|
+
return {
|
|
154
|
+
descriptor,
|
|
155
|
+
call: (args) => run(args).catch((error) => {
|
|
156
|
+
if (error instanceof QueryPathError || error instanceof ImpactError)
|
|
157
|
+
return toolError(error.message);
|
|
158
|
+
throw error;
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
201
161
|
}
|
|
202
162
|
/**
|
|
203
163
|
* Builds the function that answers one parsed JSON-RPC message.
|
|
@@ -206,8 +166,6 @@ function unknownArguments(args, allowed) {
|
|
|
206
166
|
* notification, or a response the client should not have sent.
|
|
207
167
|
*/
|
|
208
168
|
export function createMcpHandler(options) {
|
|
209
|
-
const serverInfo = { name: SERVER_NAME, version: options.version };
|
|
210
|
-
const capabilities = { tools: {}, resources: {} };
|
|
211
169
|
const readFile = options.readFile ?? ((file) => readText(nodeIo, file));
|
|
212
170
|
/** The settings a request is answered under, read when the request arrives. */
|
|
213
171
|
const current = async () => options.settings ? options.settings() : { patterns: options.patterns, run: options.run };
|
|
@@ -292,6 +250,13 @@ export function createMcpHandler(options) {
|
|
|
292
250
|
})),
|
|
293
251
|
inactiveSpecs: report.inactiveSpecs,
|
|
294
252
|
warnings: report.warnings,
|
|
253
|
+
// A document read differently from how it was written: see ADR-0010 and ADR-0002.
|
|
254
|
+
// A run always reports them; the type leaves them out only for a result a caller built.
|
|
255
|
+
specWarnings: report.specWarnings.map((warning) => ({
|
|
256
|
+
file: warning.location.relativeFile,
|
|
257
|
+
line: warning.location.line,
|
|
258
|
+
message: warning.message,
|
|
259
|
+
})),
|
|
295
260
|
exclude: report.exclude,
|
|
296
261
|
config: settings.config,
|
|
297
262
|
durationMs: Math.round(report.durationMs * 1000) / 1000,
|
|
@@ -299,36 +264,62 @@ export function createMcpHandler(options) {
|
|
|
299
264
|
const text = `Checked ${scope}.\n\n${formatReport({ ...report, config: settings.config }, { color: false, verbose: false, ascii: true }, settings.run?.maxSnippets)}`;
|
|
300
265
|
return { text, structured };
|
|
301
266
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
267
|
+
/**
|
|
268
|
+
* `spec-guard impact`, answered as `impact --json` answers: the same
|
|
269
|
+
* document, and the human report as the text. Unlike the other two tools it
|
|
270
|
+
* answers without specs, as the command does, since who imports a file does
|
|
271
|
+
* not depend on them; the report says no rules are shown.
|
|
272
|
+
*/
|
|
273
|
+
async function getDependents(args) {
|
|
274
|
+
const unknown = unknownArguments(args, ['paths', 'depth', 'include_inactive']);
|
|
275
|
+
if (unknown)
|
|
276
|
+
return unknown;
|
|
277
|
+
const paths = args['paths'];
|
|
278
|
+
if (!Array.isArray(paths) || paths.length === 0 || !paths.every((entry) => typeof entry === 'string')) {
|
|
279
|
+
return toolError('"paths" is required and must be a non-empty array of strings.');
|
|
315
280
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// cannot read.
|
|
320
|
-
outcome = toolError(error instanceof QueryPathError ? error.message : `spec-guard failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
281
|
+
const depth = args['depth'];
|
|
282
|
+
if (depth !== undefined && !(Number.isInteger(depth) && depth >= 1)) {
|
|
283
|
+
return toolError('"depth" must be a whole number of 1 or more: a depth of 0 would follow no import.');
|
|
321
284
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
285
|
+
const settings = await current();
|
|
286
|
+
const includeInactive = args['include_inactive'] ?? settings.run?.ignoreStatus ?? false;
|
|
287
|
+
if (typeof includeInactive !== 'boolean')
|
|
288
|
+
return toolError('"include_inactive" must be true or false.');
|
|
289
|
+
const report = {
|
|
290
|
+
...(await impactOf({
|
|
291
|
+
...ruleSetOptions(settings),
|
|
292
|
+
paths: paths,
|
|
293
|
+
// Absent and undefined are one answer to impactOf: every dependent.
|
|
294
|
+
depth: depth,
|
|
295
|
+
includeInactive,
|
|
296
|
+
})),
|
|
297
|
+
config: settings.config,
|
|
326
298
|
};
|
|
299
|
+
return { text: formatImpact(report), structured: { ...impactDocument(report) } };
|
|
327
300
|
}
|
|
328
|
-
|
|
329
|
-
const ruleSet = await loadRuleSet(ruleSetOptions(await current()));
|
|
301
|
+
function rulesResource(ruleSet) {
|
|
330
302
|
return {
|
|
331
|
-
|
|
303
|
+
root: toPosix(options.root),
|
|
304
|
+
specFiles: ruleSet.specFiles,
|
|
305
|
+
rules: ruleSet.rules
|
|
306
|
+
.filter(({ document }) => document.inForce)
|
|
307
|
+
.map(({ assertion, document }) => viewRule(assertion, document)),
|
|
308
|
+
notInForce: ruleSet.documents
|
|
309
|
+
.filter((document) => !document.inForce)
|
|
310
|
+
.map((document) => ({ ...viewDocument(document), rules: document.directives.length })),
|
|
311
|
+
errors: ruleSet.errors.map((error) => ({
|
|
312
|
+
file: error.location.relativeFile,
|
|
313
|
+
line: error.location.line,
|
|
314
|
+
message: error.message,
|
|
315
|
+
})),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const resources = {
|
|
319
|
+
templates: RESOURCE_TEMPLATES,
|
|
320
|
+
async list() {
|
|
321
|
+
const ruleSet = await loadRuleSet(ruleSetOptions(await current()));
|
|
322
|
+
return [
|
|
332
323
|
{
|
|
333
324
|
uri: RULES_URI,
|
|
334
325
|
name: 'rules',
|
|
@@ -346,207 +337,44 @@ export function createMcpHandler(options) {
|
|
|
346
337
|
title: document.title ?? document.relativeFile,
|
|
347
338
|
mimeType: 'text/markdown',
|
|
348
339
|
})),
|
|
349
|
-
]
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
specFiles: ruleSet.specFiles,
|
|
356
|
-
rules: ruleSet.rules
|
|
357
|
-
.filter(({ document }) => document.inForce)
|
|
358
|
-
.map(({ assertion, document }) => viewRule(assertion, document)),
|
|
359
|
-
notInForce: ruleSet.documents
|
|
360
|
-
.filter((document) => !document.inForce)
|
|
361
|
-
.map((document) => ({ ...viewDocument(document), rules: document.directives.length })),
|
|
362
|
-
errors: ruleSet.errors.map((error) => ({
|
|
363
|
-
file: error.location.relativeFile,
|
|
364
|
-
line: error.location.line,
|
|
365
|
-
message: error.message,
|
|
366
|
-
})),
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
async function readResource(params, era) {
|
|
370
|
-
const uri = params['uri'];
|
|
371
|
-
if (typeof uri !== 'string')
|
|
372
|
-
throw new ProtocolError(INVALID_PARAMS, 'resources/read needs a uri.');
|
|
373
|
-
const ruleSet = await loadRuleSet(ruleSetOptions(await current()));
|
|
374
|
-
if (uri === RULES_URI) {
|
|
375
|
-
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(rulesResource(ruleSet), null, 2) }] };
|
|
376
|
-
}
|
|
377
|
-
// Membership, never a path join: the only files this reads are the ones the
|
|
378
|
-
// spec patterns matched, so no URI can reach anything else on disk.
|
|
379
|
-
const wanted = documentPath(uri);
|
|
380
|
-
const document = ruleSet.documents.find((candidate) => candidate.relativeFile === wanted);
|
|
381
|
-
const text = document === undefined ? undefined : await readFile(document.file).catch(() => undefined);
|
|
382
|
-
if (text === undefined) {
|
|
383
|
-
throw new ProtocolError(era === 'modern' ? INVALID_PARAMS : LEGACY_RESOURCE_NOT_FOUND, 'Resource not found', { uri });
|
|
384
|
-
}
|
|
385
|
-
return { contents: [{ uri, mimeType: 'text/markdown', text }] };
|
|
386
|
-
}
|
|
387
|
-
/** A list request's cursor. This server never issues one, so any is invalid. */
|
|
388
|
-
function refuseCursor(params) {
|
|
389
|
-
if (params?.['cursor'] !== undefined)
|
|
390
|
-
throw new ProtocolError(INVALID_PARAMS, 'Invalid cursor: this server does not paginate.');
|
|
391
|
-
}
|
|
392
|
-
async function dispatch(method, params, era) {
|
|
393
|
-
const given = params ?? {};
|
|
394
|
-
if (era === 'legacy') {
|
|
395
|
-
if (method === 'initialize') {
|
|
396
|
-
return {
|
|
397
|
-
protocolVersion: negotiateLegacyVersion(given['protocolVersion']),
|
|
398
|
-
capabilities,
|
|
399
|
-
serverInfo,
|
|
400
|
-
instructions: INSTRUCTIONS,
|
|
401
|
-
};
|
|
340
|
+
];
|
|
341
|
+
},
|
|
342
|
+
async read(uri) {
|
|
343
|
+
const ruleSet = await loadRuleSet(ruleSetOptions(await current()));
|
|
344
|
+
if (uri === RULES_URI) {
|
|
345
|
+
return [{ uri, mimeType: 'application/json', text: JSON.stringify(rulesResource(ruleSet), null, 2) }];
|
|
402
346
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
case 'tools/list':
|
|
411
|
-
refuseCursor(params);
|
|
412
|
-
return { tools: TOOLS };
|
|
413
|
-
case 'tools/call':
|
|
414
|
-
return callTool(given);
|
|
415
|
-
case 'resources/list':
|
|
416
|
-
refuseCursor(params);
|
|
417
|
-
return listResources();
|
|
418
|
-
case 'resources/templates/list':
|
|
419
|
-
refuseCursor(params);
|
|
420
|
-
return { resourceTemplates: RESOURCE_TEMPLATES };
|
|
421
|
-
case 'resources/read':
|
|
422
|
-
return readResource(given, era);
|
|
423
|
-
default:
|
|
424
|
-
throw new ProtocolError(METHOD_NOT_FOUND, 'Method not found');
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
/** The 2026-07-28 shape: a result type, caching hints where required, and identity. */
|
|
428
|
-
function modernResult(method, result) {
|
|
429
|
-
// Every cacheable result is marked stale at once and private. The rules are
|
|
430
|
-
// read fresh on each request, and a client serving an ADR edited a minute ago
|
|
431
|
-
// from cache would be doing the one thing this tool exists to stop.
|
|
432
|
-
const cacheable = method !== 'tools/call';
|
|
433
|
-
return {
|
|
434
|
-
resultType: 'complete',
|
|
435
|
-
...result,
|
|
436
|
-
...(cacheable ? { ttlMs: 0, cacheScope: 'private' } : {}),
|
|
437
|
-
_meta: { [SERVER_INFO_KEY]: serverInfo },
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
return async (message) => {
|
|
441
|
-
if (!isObject(message)) {
|
|
442
|
-
return {
|
|
443
|
-
jsonrpc: '2.0',
|
|
444
|
-
error: {
|
|
445
|
-
code: INVALID_REQUEST,
|
|
446
|
-
message: Array.isArray(message) ? 'Invalid Request: batches are not supported' : 'Invalid Request',
|
|
447
|
-
},
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
const { id, method, params } = message;
|
|
451
|
-
if (message['jsonrpc'] === '2.0' && method === undefined && isRequestId(id) && ('result' in message || 'error' in message)) {
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
if (message['jsonrpc'] !== '2.0' || typeof method !== 'string' || (id !== undefined && !isRequestId(id))) {
|
|
455
|
-
return { jsonrpc: '2.0', ...(isRequestId(id) ? { id } : {}), error: { code: INVALID_REQUEST, message: 'Invalid Request' } };
|
|
456
|
-
}
|
|
457
|
-
if (id === undefined)
|
|
458
|
-
return null;
|
|
459
|
-
try {
|
|
460
|
-
if (params !== undefined && !isObject(params))
|
|
461
|
-
throw new ProtocolError(INVALID_PARAMS, 'params must be an object.');
|
|
462
|
-
const era = classifyRequest(method, params);
|
|
463
|
-
const result = await dispatch(method, params, era);
|
|
464
|
-
return { jsonrpc: '2.0', id, result: era === 'modern' ? modernResult(method, result) : result };
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
const failure = error instanceof ProtocolError
|
|
468
|
-
? { code: error.code, message: error.message, data: error.data }
|
|
469
|
-
: { code: INTERNAL_ERROR, message: `Internal error: ${error instanceof Error ? error.message : String(error)}` };
|
|
470
|
-
return { jsonrpc: '2.0', id, error: failure };
|
|
471
|
-
}
|
|
347
|
+
// Membership, never a path join: the only files this reads are the ones the
|
|
348
|
+
// spec patterns matched, so no URI can reach anything else on disk.
|
|
349
|
+
const wanted = documentPath(uri);
|
|
350
|
+
const document = ruleSet.documents.find((candidate) => candidate.relativeFile === wanted);
|
|
351
|
+
const text = document === undefined ? undefined : await readFile(document.file).catch(() => undefined);
|
|
352
|
+
return text === undefined ? null : [{ uri, mimeType: 'text/markdown', text }];
|
|
353
|
+
},
|
|
472
354
|
};
|
|
355
|
+
// Every result a modern client may cache is marked stale at once and
|
|
356
|
+
// private, which is the protocol layer's default and the one this server
|
|
357
|
+
// needs: the rules are read fresh on each request, and a client serving an
|
|
358
|
+
// ADR edited a minute ago from its cache would be doing the one thing this
|
|
359
|
+
// tool exists to stop. No prompts are declared, so prompts/list is answered
|
|
360
|
+
// "Method not found", as it is for any method the server does not have.
|
|
361
|
+
return createMcpServer({
|
|
362
|
+
name: SERVER_NAME,
|
|
363
|
+
version: options.version,
|
|
364
|
+
instructions: INSTRUCTIONS,
|
|
365
|
+
tools: [tool(GET_RULES, getRules), tool(CHECK_ARCHITECTURE, checkArchitecture), tool(GET_DEPENDENTS, getDependents)],
|
|
366
|
+
resources,
|
|
367
|
+
});
|
|
473
368
|
}
|
|
474
369
|
/* -------------------------------------------------------------------- stdio */
|
|
475
|
-
/** The key a request is tracked under: `1` and `"1"` are different ids. */
|
|
476
|
-
function trackingKey(id) {
|
|
477
|
-
return `${typeof id}:${id}`;
|
|
478
|
-
}
|
|
479
370
|
/**
|
|
480
371
|
* Serves a handler over newline-delimited JSON-RPC until the input ends.
|
|
481
372
|
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
* requires. Requests are answered concurrently and in whatever order they
|
|
486
|
-
* finish. A request the client cancels gets no response at all. When the input
|
|
487
|
-
* closes, requests still running are allowed to finish and then this resolves.
|
|
373
|
+
* spec-core's `serveLines`, under the name this module has always exported:
|
|
374
|
+
* one message per line, answered concurrently, a cancelled request answered
|
|
375
|
+
* with nothing, and the input's end awaited until the last request finishes.
|
|
488
376
|
*/
|
|
489
377
|
export function serveStdio(input, write, handle) {
|
|
490
|
-
return
|
|
491
|
-
// Decoded here rather than with setEncoding, which cannot be told apart
|
|
492
|
-
// from not setting one: Node reads an empty encoding name as UTF-8 too.
|
|
493
|
-
const decoder = new TextDecoder();
|
|
494
|
-
let buffer = '';
|
|
495
|
-
let running = 0;
|
|
496
|
-
let ended = false;
|
|
497
|
-
const inFlight = new Set();
|
|
498
|
-
const cancelled = new Set();
|
|
499
|
-
// Resolves once the input has ended and the last request has answered. A
|
|
500
|
-
// count rather than a set of promises, so that getting it wrong either hangs
|
|
501
|
-
// or ends early - both of which a test sees.
|
|
502
|
-
const settle = () => {
|
|
503
|
-
if (ended && running === 0)
|
|
504
|
-
resolve();
|
|
505
|
-
};
|
|
506
|
-
const receive = (line) => {
|
|
507
|
-
// A CRLF line keeps its \r: JSON allows it as whitespace, and a line of
|
|
508
|
-
// nothing else is blank.
|
|
509
|
-
if (line.trim() === '')
|
|
510
|
-
return;
|
|
511
|
-
let message;
|
|
512
|
-
try {
|
|
513
|
-
message = JSON.parse(line);
|
|
514
|
-
}
|
|
515
|
-
catch {
|
|
516
|
-
write(JSON.stringify({ jsonrpc: '2.0', error: { code: PARSE_ERROR, message: 'Parse error' } }));
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
if (isObject(message) && message['method'] === 'notifications/cancelled') {
|
|
520
|
-
const requestId = isObject(message['params']) ? message['params']['requestId'] : undefined;
|
|
521
|
-
if (isRequestId(requestId) && inFlight.has(trackingKey(requestId)))
|
|
522
|
-
cancelled.add(trackingKey(requestId));
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
const key = isObject(message) && typeof message['method'] === 'string' ? trackingKey(message['id']) : undefined;
|
|
526
|
-
inFlight.add(key);
|
|
527
|
-
running += 1;
|
|
528
|
-
void handle(message).then((response) => {
|
|
529
|
-
running -= 1;
|
|
530
|
-
inFlight.delete(key);
|
|
531
|
-
if (!cancelled.delete(key) && response !== null)
|
|
532
|
-
write(JSON.stringify(response));
|
|
533
|
-
settle();
|
|
534
|
-
});
|
|
535
|
-
};
|
|
536
|
-
input.on('data', (chunk) => {
|
|
537
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
538
|
-
for (let newline = buffer.indexOf('\n'); newline !== -1; newline = buffer.indexOf('\n')) {
|
|
539
|
-
const line = buffer.slice(0, newline);
|
|
540
|
-
buffer = buffer.slice(newline + 1);
|
|
541
|
-
receive(line);
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
input.once('end', () => {
|
|
545
|
-
ended = true;
|
|
546
|
-
receive(buffer);
|
|
547
|
-
settle();
|
|
548
|
-
});
|
|
549
|
-
input.once('error', reject);
|
|
550
|
-
});
|
|
378
|
+
return serveLines(input, write, handle);
|
|
551
379
|
}
|
|
552
380
|
//# sourceMappingURL=mcp.js.map
|