mcp-medic 1.0.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/LICENSE +21 -0
- package/README.md +169 -0
- package/action.yml +57 -0
- package/dist/checks/index.d.ts +6 -0
- package/dist/checks/index.js +17 -0
- package/dist/checks/malformed-schema.d.ts +2 -0
- package/dist/checks/malformed-schema.js +63 -0
- package/dist/checks/missing-description.d.ts +2 -0
- package/dist/checks/missing-description.js +71 -0
- package/dist/checks/missing-required-fields.d.ts +2 -0
- package/dist/checks/missing-required-fields.js +55 -0
- package/dist/checks/sample-call-simulation.d.ts +2 -0
- package/dist/checks/sample-call-simulation.js +239 -0
- package/dist/checks/type-mismatch.d.ts +2 -0
- package/dist/checks/type-mismatch.js +154 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +457 -0
- package/dist/config-loader.d.ts +6 -0
- package/dist/config-loader.js +77 -0
- package/dist/conformance.d.ts +10 -0
- package/dist/conformance.js +112 -0
- package/dist/discovery.d.ts +9 -0
- package/dist/discovery.js +76 -0
- package/dist/extension/index.d.ts +79 -0
- package/dist/extension/index.js +125 -0
- package/dist/fleet.d.ts +48 -0
- package/dist/fleet.js +153 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +13 -0
- package/dist/junit.d.ts +10 -0
- package/dist/junit.js +87 -0
- package/dist/orchestrator.d.ts +6 -0
- package/dist/orchestrator.js +60 -0
- package/dist/policy.d.ts +16 -0
- package/dist/policy.js +143 -0
- package/dist/protocol/connect.d.ts +2 -0
- package/dist/protocol/connect.js +417 -0
- package/dist/protocol/index.d.ts +3 -0
- package/dist/protocol/index.js +6 -0
- package/dist/registry.d.ts +16 -0
- package/dist/registry.js +87 -0
- package/dist/report.d.ts +7 -0
- package/dist/report.js +30 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +4 -0
- package/dist/watch.d.ts +12 -0
- package/dist/watch.js +85 -0
- package/package.json +56 -0
package/dist/registry.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a published MCP server descriptor or registry identifier into an MCPServerConfig.
|
|
3
|
+
*
|
|
4
|
+
* Supported formats:
|
|
5
|
+
* - Direct HTTP/SSE URL: "https://mcp.example.com/sse" -> sse/http config
|
|
6
|
+
* - NPM/Npx package: "npm:@modelcontextprotocol/server-memory" or "@modelcontextprotocol/server-memory" -> stdio npx
|
|
7
|
+
* - PyPI/Uvx package: "pypi:mcp-server-git" or "uvx:mcp-server-git" -> stdio uvx
|
|
8
|
+
* - Registry URL returning JSON server config: "https://registry.example.com/servers/my-tool.json"
|
|
9
|
+
* - Smithery / Glama server ID: "smithery:username/server-name"
|
|
10
|
+
*/
|
|
11
|
+
export async function resolveRegistryServer(registryId, options = {}) {
|
|
12
|
+
const trimmed = registryId.trim();
|
|
13
|
+
const fetchImpl = options.fetchFn ?? fetch;
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
throw new Error('Registry server identifier cannot be empty');
|
|
16
|
+
}
|
|
17
|
+
// 1. Direct URL (HTTP / SSE / JSON manifest)
|
|
18
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
19
|
+
const url = new URL(trimmed);
|
|
20
|
+
// If URL points to a JSON manifest
|
|
21
|
+
if (url.pathname.endsWith('.json')) {
|
|
22
|
+
const res = await fetchImpl(trimmed, {
|
|
23
|
+
headers: { Accept: 'application/json' },
|
|
24
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 5000),
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
throw new Error(`Failed to fetch registry descriptor from ${trimmed}: HTTP ${res.status}`);
|
|
28
|
+
}
|
|
29
|
+
const data = (await res.json());
|
|
30
|
+
if (typeof data.command === 'string' || typeof data.url === 'string') {
|
|
31
|
+
return {
|
|
32
|
+
name: (typeof data.name === 'string' && data.name) || url.pathname.split('/').pop()?.replace('.json', '') || 'registry-server',
|
|
33
|
+
transport: data.transport || (data.url ? (data.url.toString().includes('sse') ? 'sse' : 'http') : 'stdio'),
|
|
34
|
+
command: typeof data.command === 'string' ? data.command : undefined,
|
|
35
|
+
args: Array.isArray(data.args) ? data.args : undefined,
|
|
36
|
+
env: data.env || undefined,
|
|
37
|
+
url: typeof data.url === 'string' ? data.url : undefined,
|
|
38
|
+
headers: data.headers || undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Default URL endpoint transport (SSE if contains 'sse', otherwise HTTP)
|
|
43
|
+
const isSse = url.pathname.includes('sse') || url.searchParams.has('sse');
|
|
44
|
+
return {
|
|
45
|
+
name: url.hostname + url.pathname.replace(/\/$/, ''),
|
|
46
|
+
transport: isSse ? 'sse' : 'http',
|
|
47
|
+
url: trimmed,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// 2. PyPI / uvx package prefix
|
|
51
|
+
if (trimmed.startsWith('pypi:') || trimmed.startsWith('uvx:')) {
|
|
52
|
+
const pkg = trimmed.replace(/^(pypi:|uvx:)/, '');
|
|
53
|
+
return {
|
|
54
|
+
name: pkg,
|
|
55
|
+
transport: 'stdio',
|
|
56
|
+
command: 'uvx',
|
|
57
|
+
args: [pkg],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// 3. NPM package prefix
|
|
61
|
+
if (trimmed.startsWith('npm:')) {
|
|
62
|
+
const pkg = trimmed.slice(4);
|
|
63
|
+
return {
|
|
64
|
+
name: pkg,
|
|
65
|
+
transport: 'stdio',
|
|
66
|
+
command: 'npx',
|
|
67
|
+
args: ['-y', pkg],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// 4. Smithery / Glama community identifier
|
|
71
|
+
if (trimmed.startsWith('smithery:')) {
|
|
72
|
+
const serverName = trimmed.slice(9);
|
|
73
|
+
return {
|
|
74
|
+
name: `smithery/${serverName}`,
|
|
75
|
+
transport: 'stdio',
|
|
76
|
+
command: 'npx',
|
|
77
|
+
args: ['-y', `@smithery/cli@latest`, 'run', serverName],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// 5. Default scoped or standard npm package identifier (e.g. "@modelcontextprotocol/server-filesystem")
|
|
81
|
+
return {
|
|
82
|
+
name: trimmed,
|
|
83
|
+
transport: 'stdio',
|
|
84
|
+
command: 'npx',
|
|
85
|
+
args: ['-y', trimmed],
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RunReport } from './types.js';
|
|
2
|
+
export interface FormatReportOptions {
|
|
3
|
+
showFixes?: boolean;
|
|
4
|
+
}
|
|
5
|
+
/** Human-readable plain text report formatter. */
|
|
6
|
+
export declare function formatReportHuman(report: RunReport, options?: FormatReportOptions): string;
|
|
7
|
+
export declare function formatReportJSON(report: RunReport): string;
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Human-readable plain text report formatter. */
|
|
2
|
+
export function formatReportHuman(report, options = {}) {
|
|
3
|
+
const lines = [];
|
|
4
|
+
lines.push(`mcp-doctor report${report.configSource ? ` — ${report.configSource}` : ''}`);
|
|
5
|
+
lines.push(`${report.summary.connected}/${report.summary.servers} servers connected, ` +
|
|
6
|
+
`${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`);
|
|
7
|
+
lines.push('');
|
|
8
|
+
for (const conn of report.connections) {
|
|
9
|
+
const status = conn.status === 'connected' ? 'OK' : conn.status.toUpperCase();
|
|
10
|
+
lines.push(`[${status}] ${conn.server.name} (${conn.server.transport})`);
|
|
11
|
+
if (conn.error) {
|
|
12
|
+
lines.push(` ${conn.error.stage}: ${conn.error.message}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (report.diagnostics.length > 0) {
|
|
16
|
+
lines.push('');
|
|
17
|
+
lines.push('Diagnostics:');
|
|
18
|
+
for (const d of report.diagnostics) {
|
|
19
|
+
const scope = d.toolName ? `${d.serverName}/${d.toolName}` : d.serverName;
|
|
20
|
+
lines.push(` [${d.severity}] ${scope} — ${d.message} (${d.checkId})`);
|
|
21
|
+
if (options.showFixes && d.suggestedFix?.description) {
|
|
22
|
+
lines.push(` Suggested fix: ${d.suggestedFix.description}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return lines.join('\n');
|
|
27
|
+
}
|
|
28
|
+
export function formatReportJSON(report) {
|
|
29
|
+
return JSON.stringify(report, null, 2);
|
|
30
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type TransportType = 'stdio' | 'sse' | 'http';
|
|
2
|
+
export interface MCPServerConfig {
|
|
3
|
+
name: string;
|
|
4
|
+
transport: TransportType;
|
|
5
|
+
command?: string;
|
|
6
|
+
args?: string[];
|
|
7
|
+
env?: Record<string, string>;
|
|
8
|
+
url?: string;
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
tokenRefreshUrl?: string;
|
|
11
|
+
tokenRefreshBody?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface MCPConfig {
|
|
14
|
+
servers: MCPServerConfig[];
|
|
15
|
+
sourcePath?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface MCPToolDefinition {
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
inputSchema: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface MCPConnection {
|
|
23
|
+
server: MCPServerConfig;
|
|
24
|
+
status: 'connected' | 'failed' | 'timeout';
|
|
25
|
+
capabilities?: Record<string, unknown>;
|
|
26
|
+
tools?: MCPToolDefinition[];
|
|
27
|
+
error?: {
|
|
28
|
+
stage: 'spawn' | 'handshake' | 'capability-negotiation' | 'list-tools';
|
|
29
|
+
message: string;
|
|
30
|
+
raw?: unknown;
|
|
31
|
+
};
|
|
32
|
+
latencyMs?: number;
|
|
33
|
+
}
|
|
34
|
+
export type Severity = 'error' | 'warning' | 'info';
|
|
35
|
+
export interface SuggestedFix {
|
|
36
|
+
description: string;
|
|
37
|
+
patch?: unknown;
|
|
38
|
+
}
|
|
39
|
+
export interface DiagnosticResult {
|
|
40
|
+
checkId: string;
|
|
41
|
+
severity: Severity;
|
|
42
|
+
message: string;
|
|
43
|
+
serverName: string;
|
|
44
|
+
toolName?: string;
|
|
45
|
+
details?: unknown;
|
|
46
|
+
suggestedFix?: SuggestedFix;
|
|
47
|
+
}
|
|
48
|
+
export interface Check {
|
|
49
|
+
id: string;
|
|
50
|
+
description: string;
|
|
51
|
+
run(connection: MCPConnection): Promise<DiagnosticResult[]> | DiagnosticResult[];
|
|
52
|
+
}
|
|
53
|
+
export interface RunOptions {
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
checks?: Check[];
|
|
56
|
+
verbose?: boolean;
|
|
57
|
+
onLog?: (message: string) => void;
|
|
58
|
+
}
|
|
59
|
+
export interface RunReport {
|
|
60
|
+
configSource?: string;
|
|
61
|
+
connections: MCPConnection[];
|
|
62
|
+
diagnostics: DiagnosticResult[];
|
|
63
|
+
summary: {
|
|
64
|
+
servers: number;
|
|
65
|
+
connected: number;
|
|
66
|
+
failed: number;
|
|
67
|
+
errors: number;
|
|
68
|
+
warnings: number;
|
|
69
|
+
};
|
|
70
|
+
}
|
package/dist/types.js
ADDED
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface WatchOptions {
|
|
2
|
+
debounceMs?: number;
|
|
3
|
+
onTrigger: () => Promise<void> | void;
|
|
4
|
+
onError?: (err: Error) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface WatcherHandle {
|
|
7
|
+
close: () => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Watches a file with debouncing on rapid file system events.
|
|
11
|
+
*/
|
|
12
|
+
export declare function watchFileDebounced(filePath: string, options: WatchOptions): WatcherHandle;
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { watch, watchFile, unwatchFile, existsSync } from 'node:fs';
|
|
2
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Watches a file with debouncing on rapid file system events.
|
|
5
|
+
*/
|
|
6
|
+
export function watchFileDebounced(filePath, options) {
|
|
7
|
+
const debounceMs = options.debounceMs ?? 200;
|
|
8
|
+
const absPath = resolve(filePath);
|
|
9
|
+
const dirPath = dirname(absPath);
|
|
10
|
+
const targetBase = basename(absPath);
|
|
11
|
+
let timer;
|
|
12
|
+
let isClosed = false;
|
|
13
|
+
const run = () => {
|
|
14
|
+
if (isClosed)
|
|
15
|
+
return;
|
|
16
|
+
if (timer)
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
timer = setTimeout(async () => {
|
|
19
|
+
try {
|
|
20
|
+
await options.onTrigger();
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
if (options.onError) {
|
|
24
|
+
options.onError(err instanceof Error ? err : new Error(String(err)));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}, debounceMs);
|
|
28
|
+
};
|
|
29
|
+
// Watch directory to catch atomic file replaces as well as direct writes
|
|
30
|
+
let dirWatcher;
|
|
31
|
+
try {
|
|
32
|
+
if (existsSync(dirPath)) {
|
|
33
|
+
dirWatcher = watch(dirPath, (_eventType, filename) => {
|
|
34
|
+
if (!filename || filename === targetBase) {
|
|
35
|
+
run();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// fallback if dir watch fails
|
|
42
|
+
}
|
|
43
|
+
// Also watch file directly
|
|
44
|
+
let fileWatcher;
|
|
45
|
+
try {
|
|
46
|
+
if (existsSync(absPath)) {
|
|
47
|
+
fileWatcher = watch(absPath, () => {
|
|
48
|
+
run();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// fallback
|
|
54
|
+
}
|
|
55
|
+
// watchFile as polling backup
|
|
56
|
+
try {
|
|
57
|
+
if (existsSync(absPath)) {
|
|
58
|
+
watchFile(absPath, { interval: 50 }, () => {
|
|
59
|
+
run();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// fallback
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
close() {
|
|
68
|
+
isClosed = true;
|
|
69
|
+
if (timer)
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
try {
|
|
72
|
+
dirWatcher?.close();
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
try {
|
|
76
|
+
fileWatcher?.close();
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
try {
|
|
80
|
+
unwatchFile(absPath);
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-medic",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Diagnose broken MCP (Model Context Protocol) server configs before they break your agent silently.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mcp-medic": "dist/cli.js",
|
|
10
|
+
"mcpmedic": "dist/cli.js",
|
|
11
|
+
"mcp-doctor": "dist/cli.js",
|
|
12
|
+
"mcpdoctor": "dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"action.yml"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"cli",
|
|
28
|
+
"developer-tools",
|
|
29
|
+
"diagnostics",
|
|
30
|
+
"claude",
|
|
31
|
+
"claude-desktop",
|
|
32
|
+
"vscode",
|
|
33
|
+
"json-schema",
|
|
34
|
+
"linter",
|
|
35
|
+
"llm",
|
|
36
|
+
"agents"
|
|
37
|
+
],
|
|
38
|
+
"author": "Shivam Dixit",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/shivam039/mcp-doctor.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/shivam039/mcp-doctor#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/shivam039/mcp-doctor/issues"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"picocolors": "^1.1.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.0.0",
|
|
53
|
+
"typescript": "^5.5.0",
|
|
54
|
+
"vitest": "^2.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|