mcp-medic 1.2.1 → 1.2.2

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.
@@ -82,6 +82,37 @@ function normalizeTools(result) {
82
82
  };
83
83
  });
84
84
  }
85
+ function normalizeResources(result) {
86
+ if (!Array.isArray(result.resources))
87
+ throw new Error('resources/list response has no resources array');
88
+ return result.resources.map((resource, index) => {
89
+ if (!resource || typeof resource !== 'object' || typeof resource.uri !== 'string') {
90
+ throw new Error(`resources/list returned an invalid resource at index ${index}`);
91
+ }
92
+ const value = resource;
93
+ return {
94
+ uri: value.uri,
95
+ ...(typeof value.name === 'string' ? { name: value.name } : {}),
96
+ ...(typeof value.description === 'string' ? { description: value.description } : {}),
97
+ ...(typeof value.mimeType === 'string' ? { mimeType: value.mimeType } : {}),
98
+ };
99
+ });
100
+ }
101
+ function normalizePrompts(result) {
102
+ if (!Array.isArray(result.prompts))
103
+ throw new Error('prompts/list response has no prompts array');
104
+ return result.prompts.map((prompt, index) => {
105
+ if (!prompt || typeof prompt !== 'object' || typeof prompt.name !== 'string') {
106
+ throw new Error(`prompts/list returned an invalid prompt at index ${index}`);
107
+ }
108
+ const value = prompt;
109
+ return {
110
+ name: value.name,
111
+ ...(typeof value.description === 'string' ? { description: value.description } : {}),
112
+ ...(Array.isArray(value.arguments) ? { arguments: value.arguments } : {}),
113
+ };
114
+ });
115
+ }
85
116
  async function refreshTokenIfNeeded(config, options) {
86
117
  const headers = { ...(config.headers ?? {}) };
87
118
  if (!config.tokenRefreshUrl) {
@@ -409,6 +440,11 @@ export async function connect(config, timeoutMs, options) {
409
440
  else {
410
441
  return failed(config, 'failed', 'spawn', `unsupported transport: ${String(config.transport)}`);
411
442
  }
443
+ // Requests are numbered sequentially in the order they're actually sent
444
+ // (notifications don't consume an id) — tracked locally rather than
445
+ // hardcoded, since which optional capability calls happen below depends
446
+ // on what the server declares.
447
+ let nextExpectedId = 1;
412
448
  let initialize;
413
449
  try {
414
450
  const response = await withTimeout(transport.request('initialize', {
@@ -416,7 +452,7 @@ export async function connect(config, timeoutMs, options) {
416
452
  capabilities: {},
417
453
  clientInfo: CLIENT_INFO,
418
454
  }), timeoutMs, 'initialize handshake');
419
- initialize = validateResponse(response, 1);
455
+ initialize = validateResponse(response, nextExpectedId++);
420
456
  if (!initialize.capabilities || typeof initialize.capabilities !== 'object') {
421
457
  throw new Error('initialize response has no capabilities object');
422
458
  }
@@ -450,21 +486,51 @@ export async function connect(config, timeoutMs, options) {
450
486
  catch (error) {
451
487
  return failed(config, messageOf(error).includes('timed out') ? 'timeout' : 'failed', 'capability-negotiation', messageOf(error), error, { protocolVersion, serverInfo });
452
488
  }
489
+ let tools;
453
490
  try {
454
- const tools = normalizeTools(validateResponse(await withTimeout(transport.request('tools/list'), timeoutMs, 'tools/list'), 2));
455
- return {
456
- server: config,
457
- status: 'connected',
458
- capabilities,
459
- tools,
460
- protocolVersion,
461
- serverInfo,
462
- latencyMs: Date.now() - started,
463
- };
491
+ tools = normalizeTools(validateResponse(await withTimeout(transport.request('tools/list'), timeoutMs, 'tools/list'), nextExpectedId++));
464
492
  }
465
493
  catch (error) {
466
494
  return failed(config, messageOf(error).includes('timed out') ? 'timeout' : 'failed', 'list-tools', messageOf(error), error, { protocolVersion, serverInfo });
467
495
  }
496
+ // Resources and prompts are optional MCP capabilities: only inspect them
497
+ // if the server actually declared support in its initialize response.
498
+ // Passive enumeration only (resources/list, prompts/list) — never
499
+ // resources/read or prompts/get, which would be real invocation.
500
+ // A failure here is never fatal to the connection: tools is the one
501
+ // capability mcp-medic requires, so a broken resources/prompts listing
502
+ // is reported alongside a still-successful connection.
503
+ let resources;
504
+ let prompts;
505
+ const capabilityErrors = {};
506
+ if (capabilities.resources && typeof capabilities.resources === 'object') {
507
+ try {
508
+ resources = normalizeResources(validateResponse(await withTimeout(transport.request('resources/list'), timeoutMs, 'resources/list'), nextExpectedId++));
509
+ }
510
+ catch (error) {
511
+ capabilityErrors.resources = messageOf(error);
512
+ }
513
+ }
514
+ if (capabilities.prompts && typeof capabilities.prompts === 'object') {
515
+ try {
516
+ prompts = normalizePrompts(validateResponse(await withTimeout(transport.request('prompts/list'), timeoutMs, 'prompts/list'), nextExpectedId++));
517
+ }
518
+ catch (error) {
519
+ capabilityErrors.prompts = messageOf(error);
520
+ }
521
+ }
522
+ return {
523
+ server: config,
524
+ status: 'connected',
525
+ capabilities,
526
+ tools,
527
+ ...(resources !== undefined ? { resources } : {}),
528
+ ...(prompts !== undefined ? { prompts } : {}),
529
+ ...(Object.keys(capabilityErrors).length > 0 ? { capabilityErrors } : {}),
530
+ protocolVersion,
531
+ serverInfo,
532
+ latencyMs: Date.now() - started,
533
+ };
468
534
  }
469
535
  catch (error) {
470
536
  return failed(config, 'failed', 'handshake', messageOf(error), error);
package/dist/report.js CHANGED
@@ -13,6 +13,20 @@ export function formatReportHuman(report, options = {}) {
13
13
  const statusText = compatible ? '✓ compatible' : '✗ incompatible';
14
14
  lines.push(` Protocol: requested ${requested}, server negotiated ${negotiated ?? '(none)'} — ${statusText}`);
15
15
  }
16
+ if (conn.tools) {
17
+ const parts = [`${conn.tools.length} tool(s)`];
18
+ if (conn.resources)
19
+ parts.push(`${conn.resources.length} resource(s)`);
20
+ if (conn.prompts)
21
+ parts.push(`${conn.prompts.length} prompt(s)`);
22
+ lines.push(` Capabilities: ${parts.join(', ')}`);
23
+ }
24
+ if (conn.capabilityErrors?.resources) {
25
+ lines.push(` resources/list: ${conn.capabilityErrors.resources}`);
26
+ }
27
+ if (conn.capabilityErrors?.prompts) {
28
+ lines.push(` prompts/list: ${conn.capabilityErrors.prompts}`);
29
+ }
16
30
  if (conn.error) {
17
31
  lines.push(` ${conn.error.stage}: ${conn.error.message}`);
18
32
  }
package/dist/types.d.ts CHANGED
@@ -19,6 +19,17 @@ export interface MCPToolDefinition {
19
19
  description?: string;
20
20
  inputSchema: unknown;
21
21
  }
22
+ export interface MCPResourceDefinition {
23
+ uri: string;
24
+ name?: string;
25
+ description?: string;
26
+ mimeType?: string;
27
+ }
28
+ export interface MCPPromptDefinition {
29
+ name: string;
30
+ description?: string;
31
+ arguments?: unknown[];
32
+ }
22
33
  export interface ProtocolVersionInfo {
23
34
  /** The protocolVersion this client sent in `initialize`. */
24
35
  requested: string;
@@ -36,6 +47,16 @@ export interface MCPConnection {
36
47
  status: 'connected' | 'failed' | 'timeout';
37
48
  capabilities?: Record<string, unknown>;
38
49
  tools?: MCPToolDefinition[];
50
+ /** Populated only if the server's `initialize` response declared a `resources` capability. */
51
+ resources?: MCPResourceDefinition[];
52
+ /** Populated only if the server's `initialize` response declared a `prompts` capability. */
53
+ prompts?: MCPPromptDefinition[];
54
+ /** Best-effort failures from optional capability inspection (resources/prompts) — these
55
+ * never fail the overall connection, since `tools` is the one capability mcp-medic requires. */
56
+ capabilityErrors?: {
57
+ resources?: string;
58
+ prompts?: string;
59
+ };
39
60
  /** Set once an `initialize` response was received, even if negotiation was incompatible or a later stage failed. */
40
61
  protocolVersion?: ProtocolVersionInfo;
41
62
  serverInfo?: MCPServerInfo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-medic",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Diagnose broken MCP (Model Context Protocol) server configs before they break your agent silently.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",