@mcpdesc/validator 0.7.1 → 0.9.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 +35 -1
- package/README.md +15 -3
- package/index.d.ts +45 -3
- package/package.json +1 -1
- package/src/index.js +23 -2
- package/src/snapshots/0.8.0-rc.1/semantic.js +22 -10
- package/src/snapshots/0.8.0-rc.2/base.js +1417 -0
- package/src/snapshots/0.8.0-rc.2/index.js +17 -0
- package/src/snapshots/0.8.0-rc.2/schema.json +2820 -0
- package/src/snapshots/0.8.0-rc.2/semantic.js +971 -0
- package/standalone.js +1 -1
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
// Validate the immutable 0.8.0-rc.2 snapshot using only snapshot-local artifacts.
|
|
2
|
+
|
|
3
|
+
import Ajv from 'ajv';
|
|
4
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
5
|
+
import addFormats from 'ajv-formats';
|
|
6
|
+
import { UriTemplateMatcher } from 'uri-template-matcher';
|
|
7
|
+
import schema from './schema.json' with { type: 'json' };
|
|
8
|
+
import {
|
|
9
|
+
mcpExtensionCatalogue,
|
|
10
|
+
mcpExtensionMaturity,
|
|
11
|
+
semanticValidateDocument as validateBaseSemantics,
|
|
12
|
+
supportedProtocolVersions
|
|
13
|
+
} from './base.js';
|
|
14
|
+
|
|
15
|
+
export { mcpExtensionCatalogue, mcpExtensionMaturity, supportedProtocolVersions };
|
|
16
|
+
|
|
17
|
+
const TOOL_INTERACTION_SENTINEL = '__interaction_example__';
|
|
18
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
19
|
+
addFormats(ajv);
|
|
20
|
+
const ajvDraft7 = new Ajv({ allErrors: true, strict: false });
|
|
21
|
+
addFormats(ajvDraft7);
|
|
22
|
+
const validateStructure = ajv.compile(schema);
|
|
23
|
+
const primitiveCollections = ['tools', 'resources', 'resourceTemplates', 'prompts'];
|
|
24
|
+
const componentNamespaces = ['schemas', 'toolExamples', 'resourceExamples', 'resourceTemplateExamples', 'promptExamples'];
|
|
25
|
+
const knownClientCapabilities = new Set(['roots', 'sampling', 'elicitation', 'tasks', 'extensions', 'experimental']);
|
|
26
|
+
const protocolOrder = new Map(supportedProtocolVersions.map((version, index) => [version, index]));
|
|
27
|
+
|
|
28
|
+
const clientCapabilitiesByVersion = {
|
|
29
|
+
'2024-11-05': new Set(['roots', 'sampling', 'experimental']),
|
|
30
|
+
'2025-03-26': new Set(['roots', 'sampling', 'experimental']),
|
|
31
|
+
'2025-06-18': new Set(['roots', 'sampling', 'elicitation', 'experimental']),
|
|
32
|
+
'2025-11-25': new Set(['roots', 'sampling', 'elicitation', 'tasks', 'experimental']),
|
|
33
|
+
'2026-07-28': new Set(['roots', 'sampling', 'elicitation', 'extensions', 'experimental'])
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function semanticDiagnostic(code, severity, message, path) {
|
|
37
|
+
return { code, severity, message, path };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function usesMcpReservedPrefix(identifier) {
|
|
41
|
+
if (typeof identifier !== 'string' || !identifier.includes('/')) return false;
|
|
42
|
+
const labels = identifier.slice(0, identifier.indexOf('/')).split('.');
|
|
43
|
+
return labels.length >= 2 && ['modelcontextprotocol', 'mcp'].includes(labels[1].toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isReferenceObject(value) {
|
|
47
|
+
return value !== null
|
|
48
|
+
&& typeof value === 'object'
|
|
49
|
+
&& !Array.isArray(value)
|
|
50
|
+
&& Object.hasOwn(value, '$componentRef');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function componentDiagnostic(code, rel, message, path) {
|
|
54
|
+
return {
|
|
55
|
+
code,
|
|
56
|
+
severity: 'error',
|
|
57
|
+
message: `${rel}.${path.map((segment) => typeof segment === 'number' ? `[${segment}]` : segment).join('.')} ${message}`,
|
|
58
|
+
path
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveComponentReferences(document, rel = 'document') {
|
|
63
|
+
const resolved = structuredClone(document);
|
|
64
|
+
const diagnostics = [];
|
|
65
|
+
let substitutions = 0;
|
|
66
|
+
|
|
67
|
+
function resolve(reference, expectedNamespace, path, stack = []) {
|
|
68
|
+
const match = /^#\/components\/([^/]+)\/([^/]+)$/.exec(reference?.$componentRef ?? '');
|
|
69
|
+
if (!match) return reference;
|
|
70
|
+
const [, namespace, name] = match;
|
|
71
|
+
if (namespace !== expectedNamespace) {
|
|
72
|
+
diagnostics.push(componentDiagnostic(
|
|
73
|
+
'wrong-component-reference-namespace',
|
|
74
|
+
rel,
|
|
75
|
+
`must target #/components/${expectedNamespace}, not #/components/${namespace}`,
|
|
76
|
+
path
|
|
77
|
+
));
|
|
78
|
+
return reference;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const key = `${namespace}/${name}`;
|
|
82
|
+
if (stack.includes(key)) {
|
|
83
|
+
diagnostics.push(componentDiagnostic(
|
|
84
|
+
'component-reference-cycle',
|
|
85
|
+
rel,
|
|
86
|
+
`forms a cycle through ${[...stack, key].join(' -> ')}`,
|
|
87
|
+
path
|
|
88
|
+
));
|
|
89
|
+
return reference;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const target = document?.components?.[namespace]?.[name];
|
|
93
|
+
if (target === undefined) {
|
|
94
|
+
diagnostics.push(componentDiagnostic(
|
|
95
|
+
'missing-component-reference-target',
|
|
96
|
+
rel,
|
|
97
|
+
`targets missing component ${JSON.stringify(reference.$componentRef)}`,
|
|
98
|
+
path
|
|
99
|
+
));
|
|
100
|
+
return reference;
|
|
101
|
+
}
|
|
102
|
+
substitutions += 1;
|
|
103
|
+
return isReferenceObject(target)
|
|
104
|
+
? resolve(target, expectedNamespace, path, [...stack, key])
|
|
105
|
+
: structuredClone(target);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const namespace of componentNamespaces) {
|
|
109
|
+
for (const [name, value] of Object.entries(document?.components?.[namespace] ?? {})) {
|
|
110
|
+
if (isReferenceObject(value)) {
|
|
111
|
+
resolved.components[namespace][name] = resolve(value, namespace, ['components', namespace, name], [`${namespace}/${name}`]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const [collection, declarations] of Object.entries({
|
|
117
|
+
tools: document?.tools,
|
|
118
|
+
resources: document?.resources,
|
|
119
|
+
resourceTemplates: document?.resourceTemplates,
|
|
120
|
+
prompts: document?.prompts
|
|
121
|
+
})) {
|
|
122
|
+
for (const [declarationIndex, declaration] of (declarations ?? []).entries()) {
|
|
123
|
+
const resolvedDeclaration = resolved[collection][declarationIndex];
|
|
124
|
+
if (collection === 'tools') {
|
|
125
|
+
for (const field of ['inputSchema', 'outputSchema']) {
|
|
126
|
+
if (isReferenceObject(declaration[field])) {
|
|
127
|
+
resolvedDeclaration[field] = resolve(declaration[field], 'schemas', [collection, declarationIndex, field]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const exampleNamespace = collection === 'tools'
|
|
133
|
+
? 'toolExamples'
|
|
134
|
+
: collection === 'resources'
|
|
135
|
+
? 'resourceExamples'
|
|
136
|
+
: collection === 'resourceTemplates'
|
|
137
|
+
? 'resourceTemplateExamples'
|
|
138
|
+
: 'promptExamples';
|
|
139
|
+
for (const [name, example] of Object.entries(declaration.examples ?? {})) {
|
|
140
|
+
if (isReferenceObject(example)) {
|
|
141
|
+
resolvedDeclaration.examples[name] = resolve(
|
|
142
|
+
example,
|
|
143
|
+
exampleNamespace,
|
|
144
|
+
[collection, declarationIndex, 'examples', name]
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const collection of primitiveCollections) {
|
|
152
|
+
for (const [declarationIndex, declaration] of (document?.[collection] ?? []).entries()) {
|
|
153
|
+
for (const [elicitationIndex, elicitation] of (declaration.elicitations ?? []).entries()) {
|
|
154
|
+
if (isReferenceObject(elicitation.requestedSchema)) {
|
|
155
|
+
resolved[collection][declarationIndex].elicitations[elicitationIndex].requestedSchema = resolve(
|
|
156
|
+
elicitation.requestedSchema,
|
|
157
|
+
'schemas',
|
|
158
|
+
[collection, declarationIndex, 'elicitations', elicitationIndex, 'requestedSchema']
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { document: resolved, diagnostics, substitutions };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function structuralPath(document, error) {
|
|
169
|
+
const path = error.instancePath
|
|
170
|
+
? error.instancePath.slice(1).split('/').map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~'))
|
|
171
|
+
: [];
|
|
172
|
+
if (error.keyword === 'required' && typeof error.params.missingProperty === 'string') {
|
|
173
|
+
path.push(error.params.missingProperty);
|
|
174
|
+
} else if (error.keyword === 'additionalProperties' && typeof error.params.additionalProperty === 'string') {
|
|
175
|
+
path.push(error.params.additionalProperty);
|
|
176
|
+
}
|
|
177
|
+
return path.map((segment) => Array.isArray(document) && /^(?:0|[1-9][0-9]*)$/.test(segment) ? Number(segment) : segment);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function structuralDiagnostics(document) {
|
|
181
|
+
if (validateStructure(document)) return [];
|
|
182
|
+
return (validateStructure.errors ?? []).map((error) => ({
|
|
183
|
+
code: 'schema-validation',
|
|
184
|
+
severity: 'error',
|
|
185
|
+
message: `does not validate against 0.8.0: ${error.instancePath || '/'} ${error.message ?? 'unknown error'}`,
|
|
186
|
+
path: structuralPath(document, error)
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function canonicalize(value) {
|
|
191
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
192
|
+
if (value && typeof value === 'object') {
|
|
193
|
+
return Object.fromEntries(
|
|
194
|
+
Object.entries(value)
|
|
195
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
196
|
+
.map(([key, child]) => [key, canonicalize(child)])
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function canonicalString(value) {
|
|
203
|
+
return JSON.stringify(canonicalize(value));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function remapSyntheticToolExampleDiagnostic(diagnostic, toolIndex, exampleName) {
|
|
207
|
+
if (!Array.isArray(diagnostic.path)) return diagnostic;
|
|
208
|
+
const prefix = ['tools', 0, 'examples', TOOL_INTERACTION_SENTINEL];
|
|
209
|
+
const matches = prefix.every((segment, index) => diagnostic.path[index] === segment);
|
|
210
|
+
if (!matches) return diagnostic;
|
|
211
|
+
return {
|
|
212
|
+
...diagnostic,
|
|
213
|
+
message: diagnostic.message.replace(
|
|
214
|
+
`tools[0].examples[${JSON.stringify(TOOL_INTERACTION_SENTINEL)}]`,
|
|
215
|
+
`tools[${toolIndex}].interactionExamples[${JSON.stringify(exampleName)}]`
|
|
216
|
+
),
|
|
217
|
+
path: ['tools', toolIndex, 'interactionExamples', exampleName, ...diagnostic.path.slice(prefix.length)]
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function validateToolInteractionInputAndResult(tool, toolIndex, exampleName, example, scope) {
|
|
222
|
+
const synthetic = {
|
|
223
|
+
mcpdesc: '0.8.0',
|
|
224
|
+
info: { name: 'tool-interaction-validation', version: '1.0.0' },
|
|
225
|
+
protocolVersions: [...scope],
|
|
226
|
+
tools: [
|
|
227
|
+
{
|
|
228
|
+
name: tool.name,
|
|
229
|
+
inputSchema: structuredClone(tool.inputSchema),
|
|
230
|
+
...(tool.outputSchema ? { outputSchema: structuredClone(tool.outputSchema) } : {}),
|
|
231
|
+
examples: {
|
|
232
|
+
[TOOL_INTERACTION_SENTINEL]: {
|
|
233
|
+
input: structuredClone(example.input),
|
|
234
|
+
result: structuredClone(example.result)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
return validateBaseSemantics(synthetic)
|
|
241
|
+
.map((diagnostic) => remapSyntheticToolExampleDiagnostic(diagnostic, toolIndex, exampleName))
|
|
242
|
+
.filter((diagnostic) => diagnostic.path?.[0] === 'tools' && diagnostic.path?.[1] === toolIndex && diagnostic.path?.[2] === 'interactionExamples');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function validateAgainstRequestedSchema(schemaValue, value, location, path, diagnostics) {
|
|
246
|
+
try {
|
|
247
|
+
const validate = ajv.compile(schemaValue);
|
|
248
|
+
if (!validate(value)) {
|
|
249
|
+
const details = validate.errors?.map((error) => `${error.instancePath || '/'} ${error.message}`).join('; ') ?? 'unknown validation error';
|
|
250
|
+
diagnostics.push(semanticDiagnostic(
|
|
251
|
+
'interaction-example-elicitation-content-schema-mismatch',
|
|
252
|
+
'error',
|
|
253
|
+
`${location} does not validate against the elicitation request schema: ${details}`,
|
|
254
|
+
path
|
|
255
|
+
));
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
try {
|
|
259
|
+
const validate = ajvDraft7.compile(schemaValue);
|
|
260
|
+
if (!validate(value)) {
|
|
261
|
+
const details = validate.errors?.map((error) => `${error.instancePath || '/'} ${error.message}`).join('; ') ?? 'unknown validation error';
|
|
262
|
+
diagnostics.push(semanticDiagnostic(
|
|
263
|
+
'interaction-example-elicitation-content-schema-mismatch',
|
|
264
|
+
'error',
|
|
265
|
+
`${location} does not validate against the elicitation request schema: ${details}`,
|
|
266
|
+
path
|
|
267
|
+
));
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// The containing Elicitation Declaration schema validator reports malformed schemas.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function warnClientRequirementsContradiction(tool, step, stepLocation, stepPath, diagnostics) {
|
|
276
|
+
const requirements = tool.clientRequirements;
|
|
277
|
+
if (!requirements || typeof requirements !== 'object' || Array.isArray(requirements)) return;
|
|
278
|
+
|
|
279
|
+
function warn(message, path) {
|
|
280
|
+
diagnostics.push(semanticDiagnostic(
|
|
281
|
+
'interaction-example-client-requirements-contradiction',
|
|
282
|
+
'warning',
|
|
283
|
+
`${stepLocation} ${message}`,
|
|
284
|
+
path
|
|
285
|
+
));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (step.type === 'roots') {
|
|
289
|
+
if (!Object.hasOwn(requirements, 'roots')) {
|
|
290
|
+
warn('illustrates roots input, but the Tool clientRequirements do not declare roots', [...stepPath, 'type']);
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (step.type === 'sampling') {
|
|
296
|
+
if (!Object.hasOwn(requirements, 'sampling')) {
|
|
297
|
+
warn('illustrates sampling input, but the Tool clientRequirements do not declare sampling', [...stepPath, 'type']);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const sampling = requirements.sampling ?? {};
|
|
301
|
+
if ((Object.hasOwn(step.request, 'tools') || Object.hasOwn(step.request, 'toolChoice')) && !Object.hasOwn(sampling, 'tools')) {
|
|
302
|
+
warn('illustrates sampling Tool use, but the Tool clientRequirements do not declare sampling.tools', [...stepPath, 'request', Object.hasOwn(step.request, 'tools') ? 'tools' : 'toolChoice']);
|
|
303
|
+
}
|
|
304
|
+
if (step.request.includeContext && step.request.includeContext !== 'none' && !Object.hasOwn(sampling, 'context')) {
|
|
305
|
+
warn('illustrates sampling context inclusion, but the Tool clientRequirements do not declare sampling.context', [...stepPath, 'request', 'includeContext']);
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (step.type === 'elicitation') {
|
|
311
|
+
if (!Object.hasOwn(requirements, 'elicitation')) {
|
|
312
|
+
warn('illustrates elicitation input, but the Tool clientRequirements do not declare elicitation', [...stepPath, 'type']);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const elicitation = requirements.elicitation ?? {};
|
|
316
|
+
if (step.request.mode === 'url' && !Object.hasOwn(elicitation, 'url')) {
|
|
317
|
+
warn('illustrates URL elicitation, but the Tool clientRequirements do not declare elicitation.url', [...stepPath, 'request', 'mode']);
|
|
318
|
+
}
|
|
319
|
+
if (step.request.mode === 'form' && Object.keys(elicitation).length > 0 && !Object.hasOwn(elicitation, 'form')) {
|
|
320
|
+
warn('illustrates form elicitation, but the Tool clientRequirements do not declare elicitation.form', [...stepPath, 'request', 'mode']);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function validateToolInteractionExamples(document) {
|
|
326
|
+
const diagnostics = [];
|
|
327
|
+
const rootScope = document.protocolVersions ?? [];
|
|
328
|
+
|
|
329
|
+
for (const [toolIndex, tool] of (document.tools ?? []).entries()) {
|
|
330
|
+
if (!tool.interactionExamples || typeof tool.interactionExamples !== 'object' || Array.isArray(tool.interactionExamples)) continue;
|
|
331
|
+
const scope = tool.protocolVersions ?? rootScope;
|
|
332
|
+
|
|
333
|
+
for (const [exampleName, example] of Object.entries(tool.interactionExamples)) {
|
|
334
|
+
if (!example || typeof example !== 'object' || Array.isArray(example)) continue;
|
|
335
|
+
const exampleLocation = `tools[${toolIndex}].interactionExamples[${JSON.stringify(exampleName)}]`;
|
|
336
|
+
const examplePath = ['tools', toolIndex, 'interactionExamples', exampleName];
|
|
337
|
+
|
|
338
|
+
diagnostics.push(...validateToolInteractionInputAndResult(tool, toolIndex, exampleName, example, scope));
|
|
339
|
+
|
|
340
|
+
for (const [stepIndex, step] of (example.steps ?? []).entries()) {
|
|
341
|
+
const stepLocation = `${exampleLocation}.steps[${stepIndex}]`;
|
|
342
|
+
const stepPath = [...examplePath, 'steps', stepIndex];
|
|
343
|
+
warnClientRequirementsContradiction(tool, step, stepLocation, stepPath, diagnostics);
|
|
344
|
+
|
|
345
|
+
if (step.type === 'elicitation') {
|
|
346
|
+
const request = step.request ?? {};
|
|
347
|
+
const response = step.response ?? {};
|
|
348
|
+
const declaration = typeof step.declaration === 'string'
|
|
349
|
+
? (tool.elicitations ?? []).find((candidate) => candidate.name === step.declaration)
|
|
350
|
+
: undefined;
|
|
351
|
+
|
|
352
|
+
for (const version of scope) {
|
|
353
|
+
if (protocolOrder.get(version) < protocolOrder.get('2025-06-18')) {
|
|
354
|
+
diagnostics.push(semanticDiagnostic(
|
|
355
|
+
'interaction-example-version-mismatch',
|
|
356
|
+
'error',
|
|
357
|
+
`${stepLocation}.type is not defined for MCP ${version}`,
|
|
358
|
+
[...stepPath, 'type']
|
|
359
|
+
));
|
|
360
|
+
}
|
|
361
|
+
if (request.mode === 'url' && protocolOrder.get(version) < protocolOrder.get('2025-11-25')) {
|
|
362
|
+
diagnostics.push(semanticDiagnostic(
|
|
363
|
+
'interaction-example-version-mismatch',
|
|
364
|
+
'error',
|
|
365
|
+
`${stepLocation}.request.mode "url" is not defined for MCP ${version}`,
|
|
366
|
+
[...stepPath, 'request', 'mode']
|
|
367
|
+
));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (typeof step.declaration === 'string' && !declaration) {
|
|
372
|
+
diagnostics.push(semanticDiagnostic(
|
|
373
|
+
'unknown-elicitation-declaration',
|
|
374
|
+
'error',
|
|
375
|
+
`${stepLocation}.declaration identifies no Tool elicitation named ${JSON.stringify(step.declaration)}`,
|
|
376
|
+
[...stepPath, 'declaration']
|
|
377
|
+
));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (declaration) {
|
|
381
|
+
if (declaration.mode !== request.mode) {
|
|
382
|
+
diagnostics.push(semanticDiagnostic(
|
|
383
|
+
'interaction-example-elicitation-declaration-mismatch',
|
|
384
|
+
'error',
|
|
385
|
+
`${stepLocation}.request.mode ${JSON.stringify(request.mode)} is incompatible with elicitation declaration ${JSON.stringify(step.declaration)} mode ${JSON.stringify(declaration.mode)}`,
|
|
386
|
+
[...stepPath, 'request', 'mode']
|
|
387
|
+
));
|
|
388
|
+
}
|
|
389
|
+
if (request.mode === 'form' && canonicalString(declaration.requestedSchema) !== canonicalString(request.requestedSchema)) {
|
|
390
|
+
diagnostics.push(semanticDiagnostic(
|
|
391
|
+
'interaction-example-elicitation-declaration-mismatch',
|
|
392
|
+
'error',
|
|
393
|
+
`${stepLocation}.request.requestedSchema is incompatible with elicitation declaration ${JSON.stringify(step.declaration)}`,
|
|
394
|
+
[...stepPath, 'request', 'requestedSchema']
|
|
395
|
+
));
|
|
396
|
+
}
|
|
397
|
+
if (request.mode === 'url' && declaration.url !== undefined && declaration.url !== request.url) {
|
|
398
|
+
diagnostics.push(semanticDiagnostic(
|
|
399
|
+
'interaction-example-elicitation-declaration-mismatch',
|
|
400
|
+
'error',
|
|
401
|
+
`${stepLocation}.request.url is incompatible with elicitation declaration ${JSON.stringify(step.declaration)}`,
|
|
402
|
+
[...stepPath, 'request', 'url']
|
|
403
|
+
));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (request.mode === 'form') {
|
|
408
|
+
if (response.action === 'accept') {
|
|
409
|
+
if (!Object.hasOwn(response, 'content')) {
|
|
410
|
+
diagnostics.push(semanticDiagnostic(
|
|
411
|
+
'interaction-example-elicitation-response-mismatch',
|
|
412
|
+
'error',
|
|
413
|
+
`${stepLocation}.response.content is required when a form elicitation is accepted`,
|
|
414
|
+
[...stepPath, 'response', 'content']
|
|
415
|
+
));
|
|
416
|
+
} else {
|
|
417
|
+
validateAgainstRequestedSchema(
|
|
418
|
+
request.requestedSchema,
|
|
419
|
+
response.content,
|
|
420
|
+
`${stepLocation}.response.content`,
|
|
421
|
+
[...stepPath, 'response', 'content'],
|
|
422
|
+
diagnostics
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
} else if (Object.hasOwn(response, 'content')) {
|
|
426
|
+
diagnostics.push(semanticDiagnostic(
|
|
427
|
+
'interaction-example-elicitation-response-mismatch',
|
|
428
|
+
'error',
|
|
429
|
+
`${stepLocation}.response.content is not allowed when action is ${JSON.stringify(response.action)}`,
|
|
430
|
+
[...stepPath, 'response', 'content']
|
|
431
|
+
));
|
|
432
|
+
}
|
|
433
|
+
} else if (Object.hasOwn(response, 'content')) {
|
|
434
|
+
diagnostics.push(semanticDiagnostic(
|
|
435
|
+
'interaction-example-elicitation-response-mismatch',
|
|
436
|
+
'error',
|
|
437
|
+
`${stepLocation}.response.content is not allowed for URL elicitation responses`,
|
|
438
|
+
[...stepPath, 'response', 'content']
|
|
439
|
+
));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (step.type === 'sampling') {
|
|
444
|
+
for (const version of scope) {
|
|
445
|
+
if ((Object.hasOwn(step.request, 'tools') || Object.hasOwn(step.request, 'toolChoice'))
|
|
446
|
+
&& protocolOrder.get(version) < protocolOrder.get('2025-11-25')) {
|
|
447
|
+
diagnostics.push(semanticDiagnostic(
|
|
448
|
+
'interaction-example-version-mismatch',
|
|
449
|
+
'error',
|
|
450
|
+
`${stepLocation}.request uses sampling Tool fields that are not defined for MCP ${version}`,
|
|
451
|
+
[...stepPath, 'request', Object.hasOwn(step.request, 'tools') ? 'tools' : 'toolChoice']
|
|
452
|
+
));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (step.type === 'roots') {
|
|
458
|
+
for (const [rootIndex, root] of (step.response.roots ?? []).entries()) {
|
|
459
|
+
if (typeof root.uri === 'string' && !root.uri.startsWith('file://')) {
|
|
460
|
+
diagnostics.push(semanticDiagnostic(
|
|
461
|
+
'interaction-example-roots-uri',
|
|
462
|
+
'error',
|
|
463
|
+
`${stepLocation}.response.roots[${rootIndex}].uri must be a file:// URI`,
|
|
464
|
+
[...stepPath, 'response', 'roots', rootIndex, 'uri']
|
|
465
|
+
));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return diagnostics;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function validateClientRequirements(document) {
|
|
477
|
+
const diagnostics = [];
|
|
478
|
+
const rootScope = document.protocolVersions ?? [];
|
|
479
|
+
|
|
480
|
+
function invalidMember(location, path, version, member) {
|
|
481
|
+
diagnostics.push(semanticDiagnostic(
|
|
482
|
+
'client-requirement-version-mismatch',
|
|
483
|
+
'error',
|
|
484
|
+
`${location} member ${JSON.stringify(member)} is not defined by MCP ${version}`,
|
|
485
|
+
[...path, member]
|
|
486
|
+
));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function requireObject(value, location, path, version) {
|
|
490
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return true;
|
|
491
|
+
diagnostics.push(semanticDiagnostic(
|
|
492
|
+
'invalid-client-requirement-value',
|
|
493
|
+
'error',
|
|
494
|
+
`${location} must be an object for MCP ${version}`,
|
|
495
|
+
path
|
|
496
|
+
));
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function validateClosedMembers(value, allowed, location, path, version) {
|
|
501
|
+
if (!requireObject(value, location, path, version)) return false;
|
|
502
|
+
for (const member of Object.keys(value)) {
|
|
503
|
+
if (!allowed.has(member)) invalidMember(location, path, version, member);
|
|
504
|
+
}
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function validateTasks(value, location, path, version) {
|
|
509
|
+
validateClosedMembers(value, new Set(['list', 'cancel', 'requests']), location, path, version);
|
|
510
|
+
for (const member of ['list', 'cancel']) {
|
|
511
|
+
if (Object.hasOwn(value, member)) requireObject(value[member], `${location}.${member}`, [...path, member], version);
|
|
512
|
+
}
|
|
513
|
+
if (!Object.hasOwn(value, 'requests') || !requireObject(value.requests, `${location}.requests`, [...path, 'requests'], version)) return;
|
|
514
|
+
validateClosedMembers(value.requests, new Set(['sampling', 'elicitation']), `${location}.requests`, [...path, 'requests'], version);
|
|
515
|
+
for (const [family, operation] of [['sampling', 'createMessage'], ['elicitation', 'create']]) {
|
|
516
|
+
if (!Object.hasOwn(value.requests, family)) continue;
|
|
517
|
+
const familyValue = value.requests[family];
|
|
518
|
+
const familyPath = [...path, 'requests', family];
|
|
519
|
+
if (!validateClosedMembers(familyValue, new Set([operation]), `${location}.requests.${family}`, familyPath, version)) continue;
|
|
520
|
+
if (Object.hasOwn(familyValue, operation)) {
|
|
521
|
+
requireObject(familyValue[operation], `${location}.requests.${family}.${operation}`, [...familyPath, operation], version);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
for (const collection of primitiveCollections) {
|
|
527
|
+
for (const [primitiveIndex, primitive] of (document[collection] ?? []).entries()) {
|
|
528
|
+
const requirements = primitive.clientRequirements;
|
|
529
|
+
if (!requirements || typeof requirements !== 'object' || Array.isArray(requirements)) continue;
|
|
530
|
+
const scope = primitive.protocolVersions ?? rootScope;
|
|
531
|
+
const baseLocation = `${collection}[${primitiveIndex}].clientRequirements`;
|
|
532
|
+
const basePath = [collection, primitiveIndex, 'clientRequirements'];
|
|
533
|
+
|
|
534
|
+
for (const version of scope) {
|
|
535
|
+
const available = clientCapabilitiesByVersion[version] ?? new Set();
|
|
536
|
+
for (const [capability, value] of Object.entries(requirements)) {
|
|
537
|
+
if (capability.startsWith('x-')) continue;
|
|
538
|
+
if (knownClientCapabilities.has(capability) && !available.has(capability)) {
|
|
539
|
+
diagnostics.push(semanticDiagnostic(
|
|
540
|
+
'client-requirement-version-mismatch',
|
|
541
|
+
'error',
|
|
542
|
+
`${baseLocation}.${capability} is not defined by MCP ${version}; split the primitive into disjoint protocol-scoped variants when requirements differ`,
|
|
543
|
+
[...basePath, capability]
|
|
544
|
+
));
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (!available.has(capability)) continue;
|
|
548
|
+
|
|
549
|
+
const location = `${baseLocation}.${capability}`;
|
|
550
|
+
const path = [...basePath, capability];
|
|
551
|
+
if (capability === 'roots') {
|
|
552
|
+
validateClosedMembers(value, version === '2026-07-28' ? new Set() : new Set(['listChanged']), location, path, version);
|
|
553
|
+
if (version !== '2026-07-28' && Object.hasOwn(value, 'listChanged') && typeof value.listChanged !== 'boolean') {
|
|
554
|
+
diagnostics.push(semanticDiagnostic('invalid-client-requirement-value', 'error', `${location}.listChanged must be a boolean for MCP ${version}`, [...path, 'listChanged']));
|
|
555
|
+
}
|
|
556
|
+
} else if (capability === 'sampling' && ['2025-11-25', '2026-07-28'].includes(version)) {
|
|
557
|
+
validateClosedMembers(value, new Set(['context', 'tools']), location, path, version);
|
|
558
|
+
for (const member of ['context', 'tools']) {
|
|
559
|
+
if (Object.hasOwn(value, member)) requireObject(value[member], `${location}.${member}`, [...path, member], version);
|
|
560
|
+
}
|
|
561
|
+
if (Object.hasOwn(value, 'context')) {
|
|
562
|
+
diagnostics.push(semanticDiagnostic('deprecated-client-requirement', 'warning', `${location}.context uses deprecated MCP context-inclusion capability semantics in MCP ${version}`, [...path, 'context']));
|
|
563
|
+
}
|
|
564
|
+
} else if (capability === 'elicitation' && ['2025-11-25', '2026-07-28'].includes(version)) {
|
|
565
|
+
validateClosedMembers(value, new Set(['form', 'url']), location, path, version);
|
|
566
|
+
for (const member of ['form', 'url']) {
|
|
567
|
+
if (Object.hasOwn(value, member)) requireObject(value[member], `${location}.${member}`, [...path, member], version);
|
|
568
|
+
}
|
|
569
|
+
} else if (capability === 'tasks') {
|
|
570
|
+
validateTasks(value, location, path, version);
|
|
571
|
+
} else if (capability === 'extensions') {
|
|
572
|
+
for (const extension of Object.keys(value)) {
|
|
573
|
+
if (!usesMcpReservedPrefix(extension)) continue;
|
|
574
|
+
const maturity = mcpExtensionMaturity(extension);
|
|
575
|
+
if (maturity === 'official') continue;
|
|
576
|
+
if (maturity === 'experimental') {
|
|
577
|
+
diagnostics.push(semanticDiagnostic(
|
|
578
|
+
'experimental-reserved-extension-identifier',
|
|
579
|
+
'warning',
|
|
580
|
+
`${location} contains experimental MCP extension ${JSON.stringify(extension)}; preserve it and review its maturity before relying on it`,
|
|
581
|
+
[...path, extension]
|
|
582
|
+
));
|
|
583
|
+
} else {
|
|
584
|
+
diagnostics.push(semanticDiagnostic(
|
|
585
|
+
'unknown-reserved-extension-identifier',
|
|
586
|
+
'warning',
|
|
587
|
+
`${location} contains unrecognized extension ${JSON.stringify(extension)} under an MCP-reserved prefix; preserve it and review its authority`,
|
|
588
|
+
[...path, extension]
|
|
589
|
+
));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (version === '2026-07-28' && ['roots', 'sampling'].includes(capability)) {
|
|
595
|
+
diagnostics.push(semanticDiagnostic(
|
|
596
|
+
'deprecated-client-requirement',
|
|
597
|
+
'warning',
|
|
598
|
+
`${location} requires an MCP capability deprecated in MCP ${version}`,
|
|
599
|
+
path
|
|
600
|
+
));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return diagnostics;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function validatePromptContentVersion(content, contentLocation, contentPath, version, diagnostics) {
|
|
611
|
+
if (content.type === 'audio' && protocolOrder.get(version) < protocolOrder.get('2025-03-26')) {
|
|
612
|
+
diagnostics.push(semanticDiagnostic('prompt-example-content-version-mismatch', 'error', `${contentLocation} uses audio content, which is not defined for MCP ${version}`, [...contentPath, 'type']));
|
|
613
|
+
}
|
|
614
|
+
if (content.type === 'resource_link' && protocolOrder.get(version) < protocolOrder.get('2025-06-18')) {
|
|
615
|
+
diagnostics.push(semanticDiagnostic('prompt-example-content-version-mismatch', 'error', `${contentLocation} uses resource-link content, which is not defined for MCP ${version}`, [...contentPath, 'type']));
|
|
616
|
+
}
|
|
617
|
+
if (Object.hasOwn(content, '_meta') && protocolOrder.get(version) < protocolOrder.get('2025-06-18')) {
|
|
618
|
+
diagnostics.push(semanticDiagnostic('prompt-example-content-version-mismatch', 'error', `${contentLocation}._meta is not defined for MCP ${version}`, [...contentPath, '_meta']));
|
|
619
|
+
}
|
|
620
|
+
if (content.type === 'resource' && Object.hasOwn(content.resource ?? {}, '_meta') && protocolOrder.get(version) < protocolOrder.get('2025-06-18')) {
|
|
621
|
+
diagnostics.push(semanticDiagnostic('prompt-example-content-version-mismatch', 'error', `${contentLocation}.resource._meta is not defined for MCP ${version}`, [...contentPath, 'resource', '_meta']));
|
|
622
|
+
}
|
|
623
|
+
if (content.type === 'resource_link' && Array.isArray(content.icons) && protocolOrder.get(version) < protocolOrder.get('2025-11-25')) {
|
|
624
|
+
diagnostics.push(semanticDiagnostic('prompt-example-content-version-mismatch', 'error', `${contentLocation}.icons is not defined for MCP ${version}`, [...contentPath, 'icons']));
|
|
625
|
+
}
|
|
626
|
+
if (content.annotations?.lastModified !== undefined && protocolOrder.get(version) < protocolOrder.get('2025-06-18')) {
|
|
627
|
+
diagnostics.push(semanticDiagnostic('prompt-example-content-version-mismatch', 'error', `${contentLocation}.annotations.lastModified is not defined for MCP ${version}`, [...contentPath, 'annotations', 'lastModified']));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function validatePromptExamples(document) {
|
|
632
|
+
const diagnostics = [];
|
|
633
|
+
const rootScope = document.protocolVersions ?? [];
|
|
634
|
+
|
|
635
|
+
for (const [promptIndex, prompt] of (document.prompts ?? []).entries()) {
|
|
636
|
+
if (!prompt.examples || typeof prompt.examples !== 'object' || Array.isArray(prompt.examples)) continue;
|
|
637
|
+
const scope = prompt.protocolVersions ?? rootScope;
|
|
638
|
+
const declaredArguments = new Map((prompt.arguments ?? []).map((argument) => [argument.name, argument]));
|
|
639
|
+
|
|
640
|
+
for (const [exampleName, example] of Object.entries(prompt.examples)) {
|
|
641
|
+
if (!example || typeof example !== 'object' || Array.isArray(example)) continue;
|
|
642
|
+
const exampleLocation = `prompts[${promptIndex}].examples[${JSON.stringify(exampleName)}]`;
|
|
643
|
+
const examplePath = ['prompts', promptIndex, 'examples', exampleName];
|
|
644
|
+
const result = example.result ?? {};
|
|
645
|
+
const resultPath = [...examplePath, 'result'];
|
|
646
|
+
const argumentsValue = example.arguments;
|
|
647
|
+
|
|
648
|
+
if (argumentsValue && typeof argumentsValue === 'object' && !Array.isArray(argumentsValue)) {
|
|
649
|
+
for (const [argumentName] of Object.entries(argumentsValue)) {
|
|
650
|
+
if (!declaredArguments.has(argumentName)) {
|
|
651
|
+
diagnostics.push(semanticDiagnostic(
|
|
652
|
+
'prompt-example-unknown-argument',
|
|
653
|
+
'error',
|
|
654
|
+
`${exampleLocation}.arguments contains undeclared Prompt argument ${JSON.stringify(argumentName)}`,
|
|
655
|
+
[...examplePath, 'arguments', argumentName]
|
|
656
|
+
));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (const [argumentName, argument] of declaredArguments) {
|
|
662
|
+
if (argument.required === true && !Object.hasOwn(argumentsValue ?? {}, argumentName)) {
|
|
663
|
+
diagnostics.push(semanticDiagnostic(
|
|
664
|
+
'prompt-example-missing-required-argument',
|
|
665
|
+
'error',
|
|
666
|
+
`${exampleLocation}.arguments is missing required Prompt argument ${JSON.stringify(argumentName)}`,
|
|
667
|
+
[...examplePath, 'arguments', argumentName]
|
|
668
|
+
));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
for (const envelopeField of ['jsonrpc', 'id', 'error']) {
|
|
673
|
+
if (Object.hasOwn(result, envelopeField)) {
|
|
674
|
+
diagnostics.push(semanticDiagnostic('prompt-example-json-rpc-envelope', 'error', `${exampleLocation}.result must not contain JSON-RPC envelope field ${JSON.stringify(envelopeField)}`, [...resultPath, envelopeField]));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
for (const incompleteField of ['task', 'inputRequests', 'requestState']) {
|
|
678
|
+
if (Object.hasOwn(result, incompleteField)) {
|
|
679
|
+
diagnostics.push(semanticDiagnostic('incomplete-prompt-example-result', 'error', `${exampleLocation}.result must not contain non-completed workflow field ${JSON.stringify(incompleteField)}`, [...resultPath, incompleteField]));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
for (const version of scope) {
|
|
684
|
+
const resultLocation = `${exampleLocation}.result`;
|
|
685
|
+
if (version === '2026-07-28' && result.resultType !== 'complete') {
|
|
686
|
+
diagnostics.push(semanticDiagnostic('prompt-example-result-version-mismatch', 'error', `${resultLocation}.resultType must be "complete" for MCP ${version}`, [...resultPath, 'resultType']));
|
|
687
|
+
}
|
|
688
|
+
if (version !== '2026-07-28' && Object.hasOwn(result, 'resultType')) {
|
|
689
|
+
diagnostics.push(semanticDiagnostic('prompt-example-result-version-mismatch', 'error', `${resultLocation}.resultType is not defined for MCP ${version}`, [...resultPath, 'resultType']));
|
|
690
|
+
}
|
|
691
|
+
if (Object.hasOwn(result, '_meta') && protocolOrder.get(version) < protocolOrder.get('2025-06-18')) {
|
|
692
|
+
diagnostics.push(semanticDiagnostic('prompt-example-result-version-mismatch', 'error', `${resultLocation}._meta is not defined for MCP ${version}`, [...resultPath, '_meta']));
|
|
693
|
+
}
|
|
694
|
+
(result.messages ?? []).forEach((message, messageIndex) => {
|
|
695
|
+
const contentLocation = `${resultLocation}.messages[${messageIndex}].content`;
|
|
696
|
+
const contentPath = [...resultPath, 'messages', messageIndex, 'content'];
|
|
697
|
+
validatePromptContentVersion(message.content ?? {}, contentLocation, contentPath, version, diagnostics);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return diagnostics;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function extractUriTemplateVariables(uriTemplate) {
|
|
707
|
+
const matcher = new UriTemplateMatcher();
|
|
708
|
+
matcher.add(uriTemplate);
|
|
709
|
+
return new Set(
|
|
710
|
+
matcher.templates.flatMap((template) => template.parts)
|
|
711
|
+
.filter((part) => part.type === 'expression')
|
|
712
|
+
.flatMap((part) => part.expressions.map((expression) => expression.name))
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function validateCompletionExampleResult(result, exampleLocation, examplePath, version, diagnostics) {
|
|
717
|
+
const resultLocation = `${exampleLocation}.result`;
|
|
718
|
+
const resultPath = [...examplePath, 'result'];
|
|
719
|
+
|
|
720
|
+
for (const envelopeField of ['jsonrpc', 'id', 'error']) {
|
|
721
|
+
if (Object.hasOwn(result, envelopeField)) {
|
|
722
|
+
diagnostics.push(semanticDiagnostic(
|
|
723
|
+
'completion-example-json-rpc-envelope',
|
|
724
|
+
'error',
|
|
725
|
+
`${resultLocation} must not contain JSON-RPC envelope field ${JSON.stringify(envelopeField)}`,
|
|
726
|
+
[...resultPath, envelopeField]
|
|
727
|
+
));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
for (const incompleteField of ['task', 'inputRequests', 'requestState']) {
|
|
731
|
+
if (Object.hasOwn(result, incompleteField)) {
|
|
732
|
+
diagnostics.push(semanticDiagnostic(
|
|
733
|
+
'incomplete-completion-example-result',
|
|
734
|
+
'error',
|
|
735
|
+
`${resultLocation} must not contain non-completed workflow field ${JSON.stringify(incompleteField)}`,
|
|
736
|
+
[...resultPath, incompleteField]
|
|
737
|
+
));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (version === '2026-07-28' && result.resultType !== 'complete') {
|
|
741
|
+
diagnostics.push(semanticDiagnostic(
|
|
742
|
+
'completion-example-result-version-mismatch',
|
|
743
|
+
'error',
|
|
744
|
+
`${resultLocation}.resultType must be "complete" for MCP ${version}`,
|
|
745
|
+
[...resultPath, 'resultType']
|
|
746
|
+
));
|
|
747
|
+
}
|
|
748
|
+
if (version !== '2026-07-28' && Object.hasOwn(result, 'resultType')) {
|
|
749
|
+
diagnostics.push(semanticDiagnostic(
|
|
750
|
+
'completion-example-result-version-mismatch',
|
|
751
|
+
'error',
|
|
752
|
+
`${resultLocation}.resultType is not defined for MCP ${version}`,
|
|
753
|
+
[...resultPath, 'resultType']
|
|
754
|
+
));
|
|
755
|
+
}
|
|
756
|
+
if (Object.hasOwn(result, '_meta') && protocolOrder.get(version) < protocolOrder.get('2025-06-18')) {
|
|
757
|
+
diagnostics.push(semanticDiagnostic(
|
|
758
|
+
'completion-example-result-version-mismatch',
|
|
759
|
+
'error',
|
|
760
|
+
`${resultLocation}._meta is not defined for MCP ${version}`,
|
|
761
|
+
[...resultPath, '_meta']
|
|
762
|
+
));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function validateCompletionExamples(document) {
|
|
767
|
+
const diagnostics = [];
|
|
768
|
+
const rootScope = document.protocolVersions ?? [];
|
|
769
|
+
|
|
770
|
+
for (const [kind, items] of [
|
|
771
|
+
['prompts', document.prompts ?? []],
|
|
772
|
+
['resourceTemplates', document.resourceTemplates ?? []]
|
|
773
|
+
]) {
|
|
774
|
+
for (const [ownerIndex, owner] of items.entries()) {
|
|
775
|
+
if (!owner.completionExamples || typeof owner.completionExamples !== 'object' || Array.isArray(owner.completionExamples)) continue;
|
|
776
|
+
const scope = owner.protocolVersions ?? rootScope;
|
|
777
|
+
const ownerLocation = `${kind}[${ownerIndex}]`;
|
|
778
|
+
let allowedNames;
|
|
779
|
+
|
|
780
|
+
if (kind === 'prompts') {
|
|
781
|
+
allowedNames = new Set((owner.arguments ?? []).map((argument) => argument.name));
|
|
782
|
+
} else {
|
|
783
|
+
try {
|
|
784
|
+
allowedNames = extractUriTemplateVariables(owner.uriTemplate);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
diagnostics.push(semanticDiagnostic(
|
|
787
|
+
'resource-template-completion-example-invalid-template',
|
|
788
|
+
'error',
|
|
789
|
+
`${ownerLocation}.uriTemplate is not a valid RFC 6570 template: ${error.message}`,
|
|
790
|
+
[kind, ownerIndex, 'uriTemplate']
|
|
791
|
+
));
|
|
792
|
+
allowedNames = undefined;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
for (const [exampleName, example] of Object.entries(owner.completionExamples)) {
|
|
797
|
+
if (!example || typeof example !== 'object' || Array.isArray(example)) continue;
|
|
798
|
+
const exampleLocation = `${ownerLocation}.completionExamples[${JSON.stringify(exampleName)}]`;
|
|
799
|
+
const examplePath = [kind, ownerIndex, 'completionExamples', exampleName];
|
|
800
|
+
const targetName = example.argument?.name;
|
|
801
|
+
const contextArguments = example.context?.arguments;
|
|
802
|
+
|
|
803
|
+
if (typeof targetName === 'string' && allowedNames && !allowedNames.has(targetName)) {
|
|
804
|
+
diagnostics.push(semanticDiagnostic(
|
|
805
|
+
kind === 'prompts'
|
|
806
|
+
? 'prompt-completion-example-unknown-argument'
|
|
807
|
+
: 'resource-template-completion-example-unknown-variable',
|
|
808
|
+
'error',
|
|
809
|
+
kind === 'prompts'
|
|
810
|
+
? `${exampleLocation}.argument.name identifies undeclared Prompt argument ${JSON.stringify(targetName)}`
|
|
811
|
+
: `${exampleLocation}.argument.name identifies no RFC 6570 variable in ${JSON.stringify(owner.uriTemplate)}`,
|
|
812
|
+
[...examplePath, 'argument', 'name']
|
|
813
|
+
));
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (contextArguments && typeof contextArguments === 'object' && !Array.isArray(contextArguments)) {
|
|
817
|
+
for (const argumentName of Object.keys(contextArguments)) {
|
|
818
|
+
if (allowedNames && !allowedNames.has(argumentName)) {
|
|
819
|
+
diagnostics.push(semanticDiagnostic(
|
|
820
|
+
kind === 'prompts'
|
|
821
|
+
? 'prompt-completion-example-context-unknown-argument'
|
|
822
|
+
: 'resource-template-completion-example-context-unknown-variable',
|
|
823
|
+
'error',
|
|
824
|
+
kind === 'prompts'
|
|
825
|
+
? `${exampleLocation}.context.arguments contains undeclared Prompt argument ${JSON.stringify(argumentName)}`
|
|
826
|
+
: `${exampleLocation}.context.arguments contains no RFC 6570 variable ${JSON.stringify(argumentName)} from ${JSON.stringify(owner.uriTemplate)}`,
|
|
827
|
+
[...examplePath, 'context', 'arguments', argumentName]
|
|
828
|
+
));
|
|
829
|
+
}
|
|
830
|
+
if (argumentName === targetName) {
|
|
831
|
+
diagnostics.push(semanticDiagnostic(
|
|
832
|
+
'completion-example-duplicate-target-context',
|
|
833
|
+
'error',
|
|
834
|
+
`${exampleLocation}.context.arguments MUST NOT repeat the completed argument ${JSON.stringify(argumentName)}`,
|
|
835
|
+
[...examplePath, 'context', 'arguments', argumentName]
|
|
836
|
+
));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const result = example.result ?? {};
|
|
842
|
+
for (const version of scope) {
|
|
843
|
+
if (protocolOrder.get(version) < protocolOrder.get('2025-03-26')) {
|
|
844
|
+
diagnostics.push(semanticDiagnostic(
|
|
845
|
+
'completion-example-version-mismatch',
|
|
846
|
+
'error',
|
|
847
|
+
`${exampleLocation} is not defined for MCP ${version}`,
|
|
848
|
+
examplePath
|
|
849
|
+
));
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (Object.hasOwn(example, 'context') && protocolOrder.get(version) < protocolOrder.get('2025-06-18')) {
|
|
853
|
+
diagnostics.push(semanticDiagnostic(
|
|
854
|
+
'completion-example-context-version-mismatch',
|
|
855
|
+
'error',
|
|
856
|
+
`${exampleLocation}.context is not defined for MCP ${version}`,
|
|
857
|
+
[...examplePath, 'context']
|
|
858
|
+
));
|
|
859
|
+
}
|
|
860
|
+
validateCompletionExampleResult(result, exampleLocation, examplePath, version, diagnostics);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return diagnostics;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export function evaluateClientRequirements(requirements, clientCapabilities, version) {
|
|
870
|
+
if (!supportedProtocolVersions.includes(version)) {
|
|
871
|
+
throw new Error(`Unsupported MCP protocol revision ${JSON.stringify(version)}`);
|
|
872
|
+
}
|
|
873
|
+
if (requirements === undefined) {
|
|
874
|
+
return { status: 'satisfied', declared: false, unsatisfied: [], indeterminate: [] };
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const profile = clientCapabilities && typeof clientCapabilities === 'object' && !Array.isArray(clientCapabilities)
|
|
878
|
+
? clientCapabilities
|
|
879
|
+
: {};
|
|
880
|
+
const unsatisfied = [];
|
|
881
|
+
const indeterminate = [];
|
|
882
|
+
|
|
883
|
+
function requirePresence(owner, key, path) {
|
|
884
|
+
if (Object.hasOwn(owner, key)) return true;
|
|
885
|
+
unsatisfied.push(path);
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function evaluateMarkerMap(required, advertised, path) {
|
|
890
|
+
for (const [member, settings] of Object.entries(required)) {
|
|
891
|
+
const memberPath = [...path, member];
|
|
892
|
+
if (!requirePresence(advertised, member, memberPath)) continue;
|
|
893
|
+
if (Object.keys(settings).length > 0) indeterminate.push(memberPath);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
for (const [capability, required] of Object.entries(requirements)) {
|
|
898
|
+
if (capability.startsWith('x-')) continue;
|
|
899
|
+
const path = [capability];
|
|
900
|
+
if (!requirePresence(profile, capability, path)) continue;
|
|
901
|
+
const advertised = profile[capability];
|
|
902
|
+
|
|
903
|
+
if (capability === 'roots') {
|
|
904
|
+
if (Object.hasOwn(required, 'listChanged')
|
|
905
|
+
&& (!advertised || advertised.listChanged !== required.listChanged)) {
|
|
906
|
+
unsatisfied.push([...path, 'listChanged']);
|
|
907
|
+
}
|
|
908
|
+
} else if (capability === 'sampling' || capability === 'elicitation') {
|
|
909
|
+
evaluateMarkerMap(required, advertised ?? {}, path);
|
|
910
|
+
} else if (capability === 'tasks') {
|
|
911
|
+
for (const member of ['list', 'cancel']) {
|
|
912
|
+
if (Object.hasOwn(required, member)) requirePresence(advertised ?? {}, member, [...path, member]);
|
|
913
|
+
}
|
|
914
|
+
for (const [family, operation] of [['sampling', 'createMessage'], ['elicitation', 'create']]) {
|
|
915
|
+
if (!Object.hasOwn(required.requests ?? {}, family)) continue;
|
|
916
|
+
const familyPath = [...path, 'requests', family];
|
|
917
|
+
if (!requirePresence(advertised?.requests ?? {}, family, familyPath)) continue;
|
|
918
|
+
if (Object.hasOwn(required.requests[family], operation)) {
|
|
919
|
+
requirePresence(advertised.requests[family] ?? {}, operation, [...familyPath, operation]);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
} else if (capability === 'extensions') {
|
|
923
|
+
evaluateMarkerMap(required, advertised ?? {}, path);
|
|
924
|
+
} else if (capability === 'experimental') {
|
|
925
|
+
for (const member of Object.keys(required)) {
|
|
926
|
+
const memberPath = [...path, member];
|
|
927
|
+
if (requirePresence(advertised ?? {}, member, memberPath)) indeterminate.push(memberPath);
|
|
928
|
+
}
|
|
929
|
+
} else {
|
|
930
|
+
indeterminate.push(path);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return {
|
|
935
|
+
status: unsatisfied.length ? 'unsatisfied' : indeterminate.length ? 'indeterminate' : 'satisfied',
|
|
936
|
+
declared: true,
|
|
937
|
+
unsatisfied,
|
|
938
|
+
indeterminate
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export function semanticValidateDocument(document, rel = 'document') {
|
|
943
|
+
const resolution = resolveComponentReferences(document, rel);
|
|
944
|
+
const resolvedStructureDiagnostics = resolution.diagnostics.length || resolution.substitutions === 0
|
|
945
|
+
? []
|
|
946
|
+
: structuralDiagnostics(resolution.document);
|
|
947
|
+
const diagnostics = [
|
|
948
|
+
...resolution.diagnostics,
|
|
949
|
+
...resolvedStructureDiagnostics,
|
|
950
|
+
...(resolution.diagnostics.length || resolvedStructureDiagnostics.length
|
|
951
|
+
? []
|
|
952
|
+
: validateBaseSemantics(resolution.document, rel)),
|
|
953
|
+
...validateToolInteractionExamples(resolution.document),
|
|
954
|
+
...validatePromptExamples(resolution.document),
|
|
955
|
+
...validateCompletionExamples(resolution.document),
|
|
956
|
+
...validateClientRequirements(document)
|
|
957
|
+
];
|
|
958
|
+
const filtered = Object.hasOwn(document, 'transports')
|
|
959
|
+
? diagnostics
|
|
960
|
+
: diagnostics.filter((diagnostic) => diagnostic.code !== 'transport-coverage-gap');
|
|
961
|
+
return filtered.sort(
|
|
962
|
+
(left, right) => left.code.localeCompare(right.code)
|
|
963
|
+
|| left.message.localeCompare(right.message)
|
|
964
|
+
|| JSON.stringify(left.path).localeCompare(JSON.stringify(right.path))
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
export function validateMcpdesc08Document(document) {
|
|
969
|
+
const diagnostics = structuralDiagnostics(document);
|
|
970
|
+
return diagnostics.length ? diagnostics : semanticValidateDocument(document);
|
|
971
|
+
}
|