@bryan-thompson/inspector-assessment-client 1.24.2 → 1.25.1

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.
@@ -0,0 +1,782 @@
1
+ /**
2
+ * Protocol Compliance Assessor Module
3
+ *
4
+ * Unified module for MCP protocol compliance validation.
5
+ * Merges MCPSpecComplianceAssessor and ProtocolConformanceAssessor functionality.
6
+ *
7
+ * Protocol Checks:
8
+ * 1. JSON-RPC 2.0 Compliance - Validates request/response structure
9
+ * 2. Server Info Validity - Validates initialization handshake
10
+ * 3. Schema Compliance - Validates tool input schemas
11
+ * 4. Error Response Format - Validates isError flag, content array structure
12
+ * 5. Content Type Support - Validates valid content types (text, image, audio, resource)
13
+ * 6. Structured Output Support - Checks for outputSchema usage
14
+ * 7. Capabilities Compliance - Validates declared vs actual capabilities
15
+ *
16
+ * @module assessment/modules/ProtocolComplianceAssessor
17
+ */
18
+ import Ajv from "ajv";
19
+ import { BaseAssessor } from "./BaseAssessor.js";
20
+ // Valid MCP content types
21
+ const VALID_CONTENT_TYPES = [
22
+ "text",
23
+ "image",
24
+ "audio",
25
+ "resource",
26
+ "resource_link",
27
+ ];
28
+ export class ProtocolComplianceAssessor extends BaseAssessor {
29
+ ajv;
30
+ constructor(config) {
31
+ super(config);
32
+ this.ajv = new Ajv({ allErrors: true });
33
+ }
34
+ /**
35
+ * Get MCP spec version from config or use default
36
+ */
37
+ getSpecVersion() {
38
+ return this.config.mcpProtocolVersion || "2025-06";
39
+ }
40
+ /**
41
+ * Get base URL for MCP specification
42
+ */
43
+ getSpecBaseUrl() {
44
+ return `https://modelcontextprotocol.io/specification/${this.getSpecVersion()}`;
45
+ }
46
+ /**
47
+ * Assess MCP Protocol Compliance - Unified Approach
48
+ * Combines MCPSpecComplianceAssessor and ProtocolConformanceAssessor functionality
49
+ */
50
+ async assess(context) {
51
+ const protocolVersion = this.extractProtocolVersion(context);
52
+ const tools = context.tools;
53
+ const callTool = context.callTool;
54
+ // SECTION 1: Protocol Checks (from MCPSpecComplianceAssessor)
55
+ const schemaCheck = this.checkSchemaCompliance(tools);
56
+ const jsonRpcCheck = await this.checkJsonRpcCompliance(callTool);
57
+ const errorCheck = await this.checkErrorResponses(tools, callTool);
58
+ const capabilitiesCheck = this.checkCapabilitiesCompliance(context);
59
+ const protocolChecks = {
60
+ jsonRpcCompliance: {
61
+ passed: jsonRpcCheck.passed,
62
+ confidence: "high",
63
+ evidence: "Verified via actual tool call",
64
+ rawResponse: jsonRpcCheck.rawResponse,
65
+ },
66
+ serverInfoValidity: {
67
+ passed: this.checkServerInfoValidity(context.serverInfo),
68
+ confidence: "high",
69
+ evidence: "Validated server info structure",
70
+ rawResponse: context.serverInfo,
71
+ },
72
+ schemaCompliance: {
73
+ passed: schemaCheck.passed,
74
+ confidence: schemaCheck.confidence,
75
+ warnings: schemaCheck.details ? [schemaCheck.details] : undefined,
76
+ rawResponse: tools.map((t) => ({
77
+ name: t.name,
78
+ inputSchema: t.inputSchema,
79
+ })),
80
+ },
81
+ errorResponseCompliance: {
82
+ passed: errorCheck.passed,
83
+ confidence: "high",
84
+ evidence: "Tested error handling with invalid parameters",
85
+ rawResponse: errorCheck.rawResponse,
86
+ },
87
+ structuredOutputSupport: {
88
+ passed: this.checkStructuredOutputSupport(tools),
89
+ confidence: "high",
90
+ evidence: `${tools.filter((t) => t.outputSchema).length}/${tools.length} tools have outputSchema`,
91
+ rawResponse: tools.map((t) => ({
92
+ name: t.name,
93
+ hasOutputSchema: !!t.outputSchema,
94
+ outputSchema: t.outputSchema,
95
+ })),
96
+ },
97
+ capabilitiesCompliance: {
98
+ passed: capabilitiesCheck.passed,
99
+ confidence: capabilitiesCheck.confidence,
100
+ evidence: capabilitiesCheck.evidence,
101
+ warnings: capabilitiesCheck.warnings,
102
+ rawResponse: capabilitiesCheck.rawResponse,
103
+ },
104
+ };
105
+ // SECTION 2: Conformance Checks (from ProtocolConformanceAssessor)
106
+ const conformanceChecks = {
107
+ errorResponseFormat: await this.checkErrorResponseFormat(context),
108
+ contentTypeSupport: await this.checkContentTypeSupport(context),
109
+ initializationHandshake: await this.checkInitializationHandshake(context),
110
+ };
111
+ // SECTION 3: Metadata Hints (LOW CONFIDENCE - not tested, just parsed)
112
+ const metadataHints = this.extractMetadataHints(context);
113
+ // Calculate score based on all protocol checks (reliable)
114
+ const allChecks = [
115
+ ...Object.values(protocolChecks),
116
+ ...Object.values(conformanceChecks),
117
+ ];
118
+ const passedCount = allChecks.filter((c) => c.passed).length;
119
+ const totalChecks = allChecks.length;
120
+ const complianceScore = (passedCount / totalChecks) * 100;
121
+ // Track test count
122
+ this.testCount = totalChecks;
123
+ // Log score/check consistency for debugging
124
+ this.log(`Protocol Compliance: ${passedCount}/${totalChecks} checks passed (${complianceScore.toFixed(1)}%)`);
125
+ // Determine status based on protocol checks only
126
+ let status;
127
+ if (!protocolChecks.serverInfoValidity.passed) {
128
+ status = "FAIL";
129
+ }
130
+ else if (complianceScore >= 90) {
131
+ status = "PASS";
132
+ }
133
+ else if (complianceScore >= 70) {
134
+ status = "NEED_MORE_INFO";
135
+ }
136
+ else {
137
+ status = "FAIL";
138
+ }
139
+ const explanation = this.generateExplanation(complianceScore, protocolChecks, conformanceChecks);
140
+ const recommendations = this.generateRecommendations(protocolChecks, conformanceChecks, metadataHints);
141
+ // Legacy fields for backward compatibility
142
+ const transportCompliance = this.assessTransportCompliance(context);
143
+ const oauthImplementation = this.assessOAuthCompliance(context);
144
+ const annotationSupport = this.assessAnnotationSupport(context);
145
+ const streamingSupport = this.assessStreamingSupport(context);
146
+ return {
147
+ protocolVersion,
148
+ protocolChecks,
149
+ conformanceChecks,
150
+ metadataHints,
151
+ status,
152
+ complianceScore,
153
+ explanation,
154
+ recommendations,
155
+ // Legacy fields (deprecated but maintained for backward compatibility)
156
+ transportCompliance,
157
+ oauthImplementation,
158
+ annotationSupport,
159
+ streamingSupport,
160
+ };
161
+ }
162
+ /**
163
+ * Extract protocol version from context
164
+ */
165
+ extractProtocolVersion(context) {
166
+ const metadata = context.serverInfo?.metadata;
167
+ const protocolVersion = metadata?.protocolVersion;
168
+ if (protocolVersion) {
169
+ this.log(`Using protocol version from metadata: ${protocolVersion}`);
170
+ return protocolVersion;
171
+ }
172
+ if (context.serverInfo?.version) {
173
+ this.log(`Using server version as protocol version: ${context.serverInfo.version}`);
174
+ return context.serverInfo.version;
175
+ }
176
+ this.log("No protocol version information available, using default");
177
+ return "2025-06-18";
178
+ }
179
+ /**
180
+ * Check JSON-RPC 2.0 compliance
181
+ */
182
+ async checkJsonRpcCompliance(callTool) {
183
+ try {
184
+ const result = await callTool("list", {});
185
+ const hasValidStructure = result !== null &&
186
+ (Array.isArray(result.content) || result.isError !== undefined);
187
+ return { passed: hasValidStructure, rawResponse: result };
188
+ }
189
+ catch (error) {
190
+ const errorMessage = error instanceof Error ? error.message : String(error);
191
+ const isStructuredError = errorMessage.includes("MCP error") ||
192
+ errorMessage.includes("jsonrpc") ||
193
+ errorMessage.includes("-32");
194
+ return { passed: isStructuredError, rawResponse: error };
195
+ }
196
+ }
197
+ /**
198
+ * Check if server info is valid and properly formatted
199
+ */
200
+ checkServerInfoValidity(serverInfo) {
201
+ if (!serverInfo) {
202
+ return true; // No server info is acceptable (optional)
203
+ }
204
+ if (serverInfo.name !== undefined && serverInfo.name !== null) {
205
+ if (typeof serverInfo.name !== "string") {
206
+ this.log("Server info name is not a string");
207
+ return false;
208
+ }
209
+ }
210
+ if (serverInfo.metadata !== undefined && serverInfo.metadata !== null) {
211
+ if (typeof serverInfo.metadata !== "object") {
212
+ this.log("Server info metadata is not an object");
213
+ return false;
214
+ }
215
+ }
216
+ return true;
217
+ }
218
+ /**
219
+ * Check schema compliance for all tools
220
+ */
221
+ checkSchemaCompliance(tools) {
222
+ try {
223
+ let hasErrors = false;
224
+ const errors = [];
225
+ for (const tool of tools) {
226
+ if (tool.inputSchema) {
227
+ const isValid = this.ajv.validateSchema(tool.inputSchema);
228
+ if (!isValid) {
229
+ hasErrors = true;
230
+ const errorMsg = `${tool.name}: ${JSON.stringify(this.ajv.errors)}`;
231
+ errors.push(errorMsg);
232
+ console.warn(`Invalid schema for tool ${tool.name}:`, this.ajv.errors);
233
+ }
234
+ }
235
+ }
236
+ return {
237
+ passed: !hasErrors,
238
+ confidence: hasErrors ? "low" : "high",
239
+ details: hasErrors ? errors.join("; ") : undefined,
240
+ };
241
+ }
242
+ catch (error) {
243
+ console.error("Schema compliance check failed:", error);
244
+ return {
245
+ passed: false,
246
+ confidence: "low",
247
+ details: String(error),
248
+ };
249
+ }
250
+ }
251
+ /**
252
+ * Check error response compliance (basic check from MCPSpec)
253
+ */
254
+ async checkErrorResponses(tools, callTool) {
255
+ try {
256
+ if (tools.length === 0)
257
+ return { passed: true, rawResponse: "No tools to test" };
258
+ const testTool = tools[0];
259
+ try {
260
+ const result = await callTool(testTool.name, { invalid_param: "test" });
261
+ const isErrorResponse = result.isError === true;
262
+ const hasContent = Array.isArray(result.content);
263
+ const passed = (isErrorResponse && hasContent) || (!isErrorResponse && hasContent);
264
+ return { passed, rawResponse: result };
265
+ }
266
+ catch (error) {
267
+ const errorMessage = error instanceof Error ? error.message : String(error);
268
+ const isStructuredError = errorMessage.includes("MCP error") ||
269
+ errorMessage.includes("-32") ||
270
+ errorMessage.includes("jsonrpc");
271
+ return { passed: isStructuredError, rawResponse: error };
272
+ }
273
+ }
274
+ catch (error) {
275
+ return { passed: false, rawResponse: error };
276
+ }
277
+ }
278
+ /**
279
+ * Check structured output support (2025-06-18 feature)
280
+ */
281
+ checkStructuredOutputSupport(tools) {
282
+ const toolsWithOutputSchema = tools.filter((tool) => tool.outputSchema).length;
283
+ const percentage = tools.length > 0 ? (toolsWithOutputSchema / tools.length) * 100 : 0;
284
+ this.log(`Structured output support: ${toolsWithOutputSchema}/${tools.length} tools (${percentage.toFixed(1)}%)`);
285
+ return toolsWithOutputSchema > 0;
286
+ }
287
+ /**
288
+ * Check capabilities compliance
289
+ */
290
+ checkCapabilitiesCompliance(context) {
291
+ const warnings = [];
292
+ const capabilities = context.serverCapabilities;
293
+ if (!capabilities) {
294
+ return {
295
+ passed: true,
296
+ confidence: "medium",
297
+ evidence: "No server capabilities declared (optional)",
298
+ rawResponse: undefined,
299
+ };
300
+ }
301
+ if (capabilities.tools) {
302
+ if (context.tools.length === 0) {
303
+ warnings.push("Declared tools capability but no tools registered");
304
+ }
305
+ this.testCount++;
306
+ }
307
+ if (capabilities.resources) {
308
+ if (!context.resources || context.resources.length === 0) {
309
+ if (!context.readResource) {
310
+ warnings.push("Declared resources capability but no resources data provided for validation");
311
+ }
312
+ }
313
+ this.testCount++;
314
+ }
315
+ if (capabilities.prompts) {
316
+ if (!context.prompts || context.prompts.length === 0) {
317
+ if (!context.getPrompt) {
318
+ warnings.push("Declared prompts capability but no prompts data provided for validation");
319
+ }
320
+ }
321
+ this.testCount++;
322
+ }
323
+ const passed = warnings.length === 0;
324
+ const confidence = warnings.length === 0 ? "high" : "medium";
325
+ return {
326
+ passed,
327
+ confidence,
328
+ evidence: passed
329
+ ? "All declared capabilities have corresponding implementations"
330
+ : `Capability validation issues: ${warnings.join("; ")}`,
331
+ warnings: warnings.length > 0 ? warnings : undefined,
332
+ rawResponse: capabilities,
333
+ };
334
+ }
335
+ // ============================================================================
336
+ // Conformance-style checks (from ProtocolConformanceAssessor)
337
+ // ============================================================================
338
+ /**
339
+ * Select representative tools for testing (first, middle, last for diversity)
340
+ */
341
+ selectToolsForTesting(tools, maxTools = 3) {
342
+ if (tools.length <= maxTools)
343
+ return tools;
344
+ const indices = [0, Math.floor(tools.length / 2), tools.length - 1];
345
+ return [...new Set(indices)].slice(0, maxTools).map((i) => tools[i]);
346
+ }
347
+ /**
348
+ * Check Error Response Format (conformance-style with multi-tool testing)
349
+ */
350
+ async checkErrorResponseFormat(context) {
351
+ const testTools = this.selectToolsForTesting(context.tools, 3);
352
+ if (testTools.length === 0) {
353
+ return {
354
+ passed: false,
355
+ confidence: "low",
356
+ evidence: "No tools available to test error response format",
357
+ specReference: `${this.getSpecBaseUrl()}/basic/lifecycle`,
358
+ warnings: ["Cannot validate error format without tools"],
359
+ };
360
+ }
361
+ const results = [];
362
+ for (const testTool of testTools) {
363
+ try {
364
+ const result = await this.executeWithTimeout(context.callTool(testTool.name, {
365
+ __test_invalid_param__: "should_cause_error",
366
+ }), this.config.testTimeout);
367
+ const contentArray = Array.isArray(result.content)
368
+ ? result.content
369
+ : [];
370
+ const validations = {
371
+ hasIsErrorFlag: result.isError === true,
372
+ hasContentArray: Array.isArray(result.content),
373
+ contentNotEmpty: contentArray.length > 0,
374
+ firstContentHasType: contentArray[0]?.type !== undefined,
375
+ firstContentIsTextOrResource: contentArray[0]?.type === "text" ||
376
+ contentArray[0]?.type === "resource",
377
+ hasErrorMessage: typeof contentArray[0]?.text === "string" &&
378
+ contentArray[0].text.length > 0,
379
+ };
380
+ if (!result.isError && contentArray.length > 0) {
381
+ results.push({
382
+ toolName: testTool.name,
383
+ passed: true,
384
+ isErrorResponse: false,
385
+ validations,
386
+ });
387
+ }
388
+ else {
389
+ const passedValidations = Object.values(validations).filter((v) => v);
390
+ const allPassed = passedValidations.length === Object.keys(validations).length;
391
+ results.push({
392
+ toolName: testTool.name,
393
+ passed: allPassed,
394
+ isErrorResponse: true,
395
+ validations,
396
+ });
397
+ }
398
+ }
399
+ catch (error) {
400
+ results.push({
401
+ toolName: testTool.name,
402
+ passed: false,
403
+ isErrorResponse: false,
404
+ error: error instanceof Error ? error.message : String(error),
405
+ });
406
+ }
407
+ }
408
+ const errorResponseResults = results.filter((r) => r.isErrorResponse);
409
+ const passedCount = results.filter((r) => r.passed).length;
410
+ const allPassed = passedCount === results.length;
411
+ let confidence;
412
+ if (errorResponseResults.length === 0) {
413
+ confidence = "medium";
414
+ }
415
+ else if (allPassed) {
416
+ confidence = "high";
417
+ }
418
+ else {
419
+ confidence = "medium";
420
+ }
421
+ return {
422
+ passed: allPassed,
423
+ confidence,
424
+ evidence: `Tested ${results.length} tool(s): ${passedCount}/${results.length} passed error format validation`,
425
+ specReference: `${this.getSpecBaseUrl()}/basic/lifecycle`,
426
+ details: {
427
+ toolResults: results,
428
+ testedToolCount: results.length,
429
+ errorResponseCount: errorResponseResults.length,
430
+ },
431
+ warnings: allPassed
432
+ ? undefined
433
+ : [
434
+ "Error response format issues detected in some tools",
435
+ "Ensure all errors have isError: true and content array with text type",
436
+ ],
437
+ };
438
+ }
439
+ /**
440
+ * Check Content Type Support
441
+ */
442
+ async checkContentTypeSupport(context) {
443
+ try {
444
+ const testTool = context.tools[0];
445
+ if (!testTool) {
446
+ return {
447
+ passed: false,
448
+ confidence: "low",
449
+ evidence: "No tools available to test content types",
450
+ specReference: `${this.getSpecBaseUrl()}/server/tools`,
451
+ };
452
+ }
453
+ const schema = testTool.inputSchema;
454
+ const hasRequiredParams = schema?.required &&
455
+ Array.isArray(schema.required) &&
456
+ schema.required.length > 0;
457
+ if (hasRequiredParams) {
458
+ return {
459
+ passed: true,
460
+ confidence: "low",
461
+ evidence: "Cannot test content types without knowing valid parameters - tool has required params",
462
+ specReference: `${this.getSpecBaseUrl()}/server/tools`,
463
+ warnings: ["Content type validation requires valid tool parameters"],
464
+ };
465
+ }
466
+ const result = await this.executeWithTimeout(context.callTool(testTool.name, {}), this.config.testTimeout);
467
+ const contentArray = Array.isArray(result.content) ? result.content : [];
468
+ const validations = {
469
+ hasContentArray: Array.isArray(result.content),
470
+ contentNotEmpty: contentArray.length > 0,
471
+ allContentHasType: contentArray.every((c) => c.type !== undefined),
472
+ validContentTypes: contentArray.every((c) => VALID_CONTENT_TYPES.includes(c.type)),
473
+ };
474
+ const passedValidations = Object.values(validations).filter((v) => v);
475
+ const allPassed = passedValidations.length === Object.keys(validations).length;
476
+ const detectedTypes = contentArray.map((c) => c.type);
477
+ const invalidTypes = detectedTypes.filter((t) => !VALID_CONTENT_TYPES.includes(t));
478
+ return {
479
+ passed: allPassed,
480
+ confidence: allPassed ? "high" : "medium",
481
+ evidence: `${passedValidations.length}/${Object.keys(validations).length} content type checks passed`,
482
+ specReference: `${this.getSpecBaseUrl()}/server/tools`,
483
+ details: {
484
+ validations,
485
+ detectedContentTypes: detectedTypes,
486
+ invalidContentTypes: invalidTypes.length > 0 ? invalidTypes : undefined,
487
+ },
488
+ warnings: invalidTypes.length > 0
489
+ ? [`Invalid content types found: ${invalidTypes.join(", ")}`]
490
+ : undefined,
491
+ };
492
+ }
493
+ catch (error) {
494
+ return {
495
+ passed: false,
496
+ confidence: "medium",
497
+ evidence: "Could not test content types due to error",
498
+ specReference: `${this.getSpecBaseUrl()}/server/tools`,
499
+ details: {
500
+ error: error instanceof Error ? error.message : String(error),
501
+ },
502
+ };
503
+ }
504
+ }
505
+ /**
506
+ * Check Initialization Handshake
507
+ */
508
+ async checkInitializationHandshake(context) {
509
+ const serverInfo = context.serverInfo;
510
+ const serverCapabilities = context.serverCapabilities;
511
+ const validations = {
512
+ hasServerInfo: serverInfo !== undefined && serverInfo !== null,
513
+ hasServerName: typeof serverInfo?.name === "string" && serverInfo.name.length > 0,
514
+ hasServerVersion: typeof serverInfo?.version === "string" &&
515
+ serverInfo.version.length > 0,
516
+ hasCapabilities: serverCapabilities !== undefined,
517
+ };
518
+ const passedValidations = Object.values(validations).filter((v) => v);
519
+ const allPassed = passedValidations.length === Object.keys(validations).length;
520
+ const hasMinimumInfo = validations.hasServerInfo && validations.hasServerName;
521
+ return {
522
+ passed: hasMinimumInfo,
523
+ confidence: allPassed ? "high" : "medium",
524
+ evidence: `${passedValidations.length}/${Object.keys(validations).length} initialization checks passed`,
525
+ specReference: `${this.getSpecBaseUrl()}/basic/lifecycle`,
526
+ details: {
527
+ validations,
528
+ serverInfo: {
529
+ name: serverInfo?.name,
530
+ version: serverInfo?.version,
531
+ hasCapabilities: !!serverCapabilities,
532
+ },
533
+ },
534
+ warnings: !allPassed
535
+ ? [
536
+ !validations.hasServerVersion
537
+ ? "Server should provide version for better compatibility tracking"
538
+ : undefined,
539
+ !validations.hasCapabilities
540
+ ? "Server should declare capabilities for feature negotiation"
541
+ : undefined,
542
+ ].filter(Boolean)
543
+ : undefined,
544
+ };
545
+ }
546
+ // ============================================================================
547
+ // Legacy compatibility methods (from MCPSpecComplianceAssessor)
548
+ // ============================================================================
549
+ assessTransportCompliance(context) {
550
+ if (!context.serverInfo) {
551
+ return {
552
+ supportsStreamableHTTP: false,
553
+ deprecatedSSE: false,
554
+ transportValidation: "failed",
555
+ supportsStdio: false,
556
+ supportsSSE: false,
557
+ confidence: "low",
558
+ detectionMethod: "manual-required",
559
+ requiresManualCheck: true,
560
+ manualVerificationSteps: [
561
+ "Test STDIO: Run `npm start`, send JSON-RPC initialize request",
562
+ "Test HTTP: Set HTTP_STREAMABLE_SERVER=true, run `npm start`, test /health endpoint",
563
+ "Check if framework handles transports internally",
564
+ ],
565
+ };
566
+ }
567
+ const metadata = context.serverInfo?.metadata;
568
+ const transport = metadata?.transport;
569
+ const hasTransportMetadata = !!transport;
570
+ const supportsStreamableHTTP = transport === "streamable-http" ||
571
+ transport === "http" ||
572
+ (!transport && !!context.serverInfo);
573
+ const deprecatedSSE = transport === "sse";
574
+ let transportValidation = "passed";
575
+ if (deprecatedSSE) {
576
+ transportValidation = "partial";
577
+ }
578
+ else if (transport &&
579
+ transport !== "streamable-http" &&
580
+ transport !== "http" &&
581
+ transport !== "stdio") {
582
+ transportValidation = "failed";
583
+ }
584
+ const confidence = hasTransportMetadata ? "medium" : "low";
585
+ const requiresManualCheck = !hasTransportMetadata;
586
+ return {
587
+ supportsStreamableHTTP,
588
+ deprecatedSSE,
589
+ transportValidation,
590
+ supportsStdio: transport === "stdio" || !transport,
591
+ supportsSSE: deprecatedSSE,
592
+ confidence,
593
+ detectionMethod: hasTransportMetadata ? "automated" : "manual-required",
594
+ requiresManualCheck,
595
+ manualVerificationSteps: requiresManualCheck
596
+ ? [
597
+ "Test STDIO: Run `npm start`, send JSON-RPC initialize request via stdin",
598
+ "Test HTTP: Set HTTP_STREAMABLE_SERVER=true, run `npm start`, curl http://localhost:3000/health",
599
+ ]
600
+ : undefined,
601
+ };
602
+ }
603
+ assessAnnotationSupport(context) {
604
+ const metadata = context.serverInfo?.metadata;
605
+ const annotations = metadata?.annotations;
606
+ const supportsAnnotations = annotations?.supported || false;
607
+ const customAnnotations = annotations?.types || [];
608
+ return {
609
+ supportsReadOnlyHint: supportsAnnotations,
610
+ supportsDestructiveHint: supportsAnnotations,
611
+ supportsTitleAnnotation: supportsAnnotations,
612
+ customAnnotations: customAnnotations.length > 0 ? customAnnotations : undefined,
613
+ };
614
+ }
615
+ assessStreamingSupport(context) {
616
+ const metadata = context.serverInfo?.metadata;
617
+ const streaming = metadata?.streaming;
618
+ const supportsStreaming = streaming?.supported || false;
619
+ const protocol = streaming?.protocol;
620
+ const validProtocols = ["http-streaming", "sse", "websocket"];
621
+ const streamingProtocol = protocol && validProtocols.includes(protocol)
622
+ ? protocol
623
+ : supportsStreaming
624
+ ? "http-streaming"
625
+ : undefined;
626
+ return {
627
+ supportsStreaming,
628
+ streamingProtocol,
629
+ };
630
+ }
631
+ assessOAuthCompliance(context) {
632
+ const metadata = context.serverInfo?.metadata;
633
+ const oauthConfig = metadata?.oauth;
634
+ if (!oauthConfig || !oauthConfig.enabled) {
635
+ return undefined;
636
+ }
637
+ const resourceIndicators = [];
638
+ if (oauthConfig.resourceIndicators) {
639
+ const indicators = oauthConfig.resourceIndicators;
640
+ resourceIndicators.push(...indicators);
641
+ }
642
+ if (oauthConfig.resourceServer) {
643
+ resourceIndicators.push(oauthConfig.resourceServer);
644
+ }
645
+ return {
646
+ implementsResourceServer: oauthConfig.enabled === true,
647
+ supportsRFC8707: oauthConfig.supportsRFC8707 || false,
648
+ resourceIndicators,
649
+ tokenValidation: oauthConfig.tokenValidation !== false,
650
+ scopeEnforcement: oauthConfig.scopeEnforcement !== false,
651
+ supportsOAuth: oauthConfig.enabled === true,
652
+ supportsPKCE: oauthConfig.supportsPKCE || false,
653
+ };
654
+ }
655
+ extractMetadataHints(context) {
656
+ const metadata = context.serverInfo?.metadata;
657
+ if (!metadata && !context.serverInfo) {
658
+ return undefined;
659
+ }
660
+ const transport = metadata?.transport;
661
+ const transportHints = {
662
+ detectedTransport: transport,
663
+ supportsStdio: transport === "stdio" || !transport,
664
+ supportsHTTP: transport === "http" ||
665
+ transport === "streamable-http" ||
666
+ (!transport && !!context.serverInfo),
667
+ supportsSSE: transport === "sse",
668
+ detectionMethod: (transport ? "metadata" : "assumed"),
669
+ };
670
+ const oauthConfig = metadata?.oauth;
671
+ const oauthHints = oauthConfig
672
+ ? {
673
+ hasOAuthConfig: true,
674
+ supportsOAuth: oauthConfig.enabled === true,
675
+ supportsPKCE: oauthConfig.supportsPKCE || false,
676
+ resourceIndicators: oauthConfig.resourceIndicators
677
+ ? oauthConfig.resourceIndicators
678
+ : undefined,
679
+ }
680
+ : undefined;
681
+ const annotations = metadata?.annotations;
682
+ const annotationHints = {
683
+ supportsReadOnlyHint: annotations?.supported || false,
684
+ supportsDestructiveHint: annotations?.supported || false,
685
+ supportsTitleAnnotation: annotations?.supported || false,
686
+ customAnnotations: annotations?.types
687
+ ? annotations.types
688
+ : undefined,
689
+ };
690
+ const streaming = metadata?.streaming;
691
+ const streamingHints = {
692
+ supportsStreaming: streaming?.supported || false,
693
+ streamingProtocol: streaming?.protocol &&
694
+ ["http-streaming", "sse", "websocket"].includes(streaming.protocol)
695
+ ? streaming.protocol
696
+ : undefined,
697
+ };
698
+ return {
699
+ confidence: "low",
700
+ requiresManualVerification: true,
701
+ transportHints,
702
+ oauthHints,
703
+ annotationHints,
704
+ streamingHints,
705
+ manualVerificationSteps: [
706
+ "Test STDIO transport: Run `npm start`, send JSON-RPC initialize request via stdin",
707
+ "Test HTTP transport: Set HTTP_STREAMABLE_SERVER=true, run `npm start`, curl http://localhost:3000/health",
708
+ "Verify OAuth endpoints if configured",
709
+ ],
710
+ };
711
+ }
712
+ /**
713
+ * Generate explanation based on all protocol checks
714
+ */
715
+ generateExplanation(complianceScore, protocolChecks, conformanceChecks) {
716
+ const failedChecks = [];
717
+ if (!protocolChecks.jsonRpcCompliance.passed)
718
+ failedChecks.push("JSON-RPC compliance");
719
+ if (!protocolChecks.serverInfoValidity.passed)
720
+ failedChecks.push("server info validity");
721
+ if (!protocolChecks.schemaCompliance.passed)
722
+ failedChecks.push("schema compliance");
723
+ if (!protocolChecks.errorResponseCompliance.passed)
724
+ failedChecks.push("error response compliance");
725
+ Object.entries(conformanceChecks).forEach(([name, check]) => {
726
+ if (!check.passed) {
727
+ failedChecks.push(name
728
+ .replace(/([A-Z])/g, " $1")
729
+ .toLowerCase()
730
+ .trim());
731
+ }
732
+ });
733
+ if (complianceScore >= 90) {
734
+ return "Excellent MCP protocol compliance. Server meets all critical requirements verified through protocol testing.";
735
+ }
736
+ else if (complianceScore >= 70) {
737
+ return `Good MCP compliance with minor issues: ${failedChecks.join(", ")}. Review recommended before directory submission.`;
738
+ }
739
+ else {
740
+ return `Poor MCP compliance detected. Critical issues: ${failedChecks.join(", ")}. Must fix before directory approval.`;
741
+ }
742
+ }
743
+ /**
744
+ * Generate recommendations based on all checks
745
+ */
746
+ generateRecommendations(protocolChecks, conformanceChecks, metadataHints) {
747
+ const recommendations = [];
748
+ if (!protocolChecks.jsonRpcCompliance.passed) {
749
+ recommendations.push("Ensure all requests/responses follow JSON-RPC 2.0 format with proper jsonrpc, id, method/result fields.");
750
+ }
751
+ if (!protocolChecks.serverInfoValidity.passed) {
752
+ recommendations.push("Fix serverInfo structure to include valid name and metadata fields.");
753
+ }
754
+ if (!protocolChecks.schemaCompliance.passed) {
755
+ if (protocolChecks.schemaCompliance.confidence === "low") {
756
+ recommendations.push("Schema validation warnings detected (may be false positives from Zod/TypeBox conversion).");
757
+ }
758
+ else {
759
+ recommendations.push("Review tool schemas and ensure they follow JSON Schema specification.");
760
+ }
761
+ }
762
+ if (!conformanceChecks.errorResponseFormat.passed) {
763
+ recommendations.push("Ensure error responses include 'isError: true' flag and properly formatted content array.");
764
+ }
765
+ if (!conformanceChecks.contentTypeSupport.passed) {
766
+ recommendations.push("Use only valid content types: text, image, audio, resource, resource_link.");
767
+ }
768
+ if (!conformanceChecks.initializationHandshake.passed) {
769
+ recommendations.push("Ensure server provides name and version during initialization.");
770
+ }
771
+ if (!protocolChecks.structuredOutputSupport.passed) {
772
+ recommendations.push("Consider adding outputSchema to tools for type-safe responses (optional MCP 2025-06-18 feature).");
773
+ }
774
+ if (metadataHints?.requiresManualVerification) {
775
+ recommendations.push("Transport/OAuth/Streaming features require manual verification (metadata-based detection only).");
776
+ }
777
+ if (recommendations.length === 0) {
778
+ recommendations.push("Excellent MCP compliance! All protocol checks passed. Server is ready for directory submission.");
779
+ }
780
+ return recommendations;
781
+ }
782
+ }