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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/action.yml +57 -0
  4. package/dist/checks/index.d.ts +6 -0
  5. package/dist/checks/index.js +17 -0
  6. package/dist/checks/malformed-schema.d.ts +2 -0
  7. package/dist/checks/malformed-schema.js +63 -0
  8. package/dist/checks/missing-description.d.ts +2 -0
  9. package/dist/checks/missing-description.js +71 -0
  10. package/dist/checks/missing-required-fields.d.ts +2 -0
  11. package/dist/checks/missing-required-fields.js +55 -0
  12. package/dist/checks/sample-call-simulation.d.ts +2 -0
  13. package/dist/checks/sample-call-simulation.js +239 -0
  14. package/dist/checks/type-mismatch.d.ts +2 -0
  15. package/dist/checks/type-mismatch.js +154 -0
  16. package/dist/cli.d.ts +20 -0
  17. package/dist/cli.js +457 -0
  18. package/dist/config-loader.d.ts +6 -0
  19. package/dist/config-loader.js +77 -0
  20. package/dist/conformance.d.ts +10 -0
  21. package/dist/conformance.js +112 -0
  22. package/dist/discovery.d.ts +9 -0
  23. package/dist/discovery.js +76 -0
  24. package/dist/extension/index.d.ts +79 -0
  25. package/dist/extension/index.js +125 -0
  26. package/dist/fleet.d.ts +48 -0
  27. package/dist/fleet.js +153 -0
  28. package/dist/index.d.ts +20 -0
  29. package/dist/index.js +13 -0
  30. package/dist/junit.d.ts +10 -0
  31. package/dist/junit.js +87 -0
  32. package/dist/orchestrator.d.ts +6 -0
  33. package/dist/orchestrator.js +60 -0
  34. package/dist/policy.d.ts +16 -0
  35. package/dist/policy.js +143 -0
  36. package/dist/protocol/connect.d.ts +2 -0
  37. package/dist/protocol/connect.js +417 -0
  38. package/dist/protocol/index.d.ts +3 -0
  39. package/dist/protocol/index.js +6 -0
  40. package/dist/registry.d.ts +16 -0
  41. package/dist/registry.js +87 -0
  42. package/dist/report.d.ts +7 -0
  43. package/dist/report.js +30 -0
  44. package/dist/types.d.ts +70 -0
  45. package/dist/types.js +4 -0
  46. package/dist/watch.d.ts +12 -0
  47. package/dist/watch.js +85 -0
  48. package/package.json +56 -0
@@ -0,0 +1,239 @@
1
+ function generatePlaceholderForType(propDef) {
2
+ // 1. If enum is specified
3
+ if (Array.isArray(propDef.enum)) {
4
+ if (propDef.enum.length === 0) {
5
+ throw new Error('required enum array is empty');
6
+ }
7
+ return { value: propDef.enum[0] };
8
+ }
9
+ // 2. If default is specified
10
+ if (propDef.default !== undefined) {
11
+ return { value: propDef.default };
12
+ }
13
+ // 3. Match by type
14
+ const rawType = propDef.type;
15
+ const targetType = Array.isArray(rawType) ? rawType[0] : rawType;
16
+ if (targetType === 'string') {
17
+ if (typeof propDef.minLength === 'number' && propDef.minLength > 0) {
18
+ return { value: 'a'.repeat(propDef.minLength) };
19
+ }
20
+ return { value: '' };
21
+ }
22
+ if (targetType === 'number' || targetType === 'integer') {
23
+ if (typeof propDef.minimum === 'number' && propDef.minimum > 0) {
24
+ return { value: propDef.minimum };
25
+ }
26
+ return { value: 0 };
27
+ }
28
+ if (targetType === 'boolean') {
29
+ return { value: false };
30
+ }
31
+ if (targetType === 'array') {
32
+ return { value: [] };
33
+ }
34
+ if (targetType === 'object') {
35
+ return { value: {} };
36
+ }
37
+ if (targetType === 'null') {
38
+ return { value: null };
39
+ }
40
+ // Fallback if type is missing or complex
41
+ return {
42
+ value: '',
43
+ caveat: `unknown or missing type "${String(rawType)}", defaulted to empty string`,
44
+ };
45
+ }
46
+ function validateSyntheticPayload(payload, schema) {
47
+ const errors = [];
48
+ const caveats = [];
49
+ const required = Array.isArray(schema.required) ? schema.required : [];
50
+ const properties = schema.properties && typeof schema.properties === 'object' && !Array.isArray(schema.properties)
51
+ ? schema.properties
52
+ : {};
53
+ // Verify all required fields are present
54
+ for (const req of required) {
55
+ if (typeof req === 'string' && !(req in payload)) {
56
+ errors.push(`Missing required field "${req}" in synthetic payload.`);
57
+ }
58
+ }
59
+ // Validate properties
60
+ for (const [key, val] of Object.entries(payload)) {
61
+ const propDef = properties[key];
62
+ if (!propDef || typeof propDef !== 'object' || Array.isArray(propDef)) {
63
+ continue;
64
+ }
65
+ const propObj = propDef;
66
+ // Enum validation
67
+ if (Array.isArray(propObj.enum)) {
68
+ if (propObj.enum.length === 0) {
69
+ errors.push(`Field "${key}" specifies an empty enum array.`);
70
+ }
71
+ else if (!propObj.enum.includes(val)) {
72
+ errors.push(`Field "${key}" value ${JSON.stringify(val)} is not in enum [${propObj.enum.map((e) => JSON.stringify(e)).join(', ')}].`);
73
+ }
74
+ }
75
+ // Number constraints check
76
+ if (typeof val === 'number') {
77
+ if (typeof propObj.minimum === 'number' && typeof propObj.maximum === 'number') {
78
+ if (propObj.minimum > propObj.maximum) {
79
+ errors.push(`Field "${key}" has contradictory bounds: minimum (${propObj.minimum}) > maximum (${propObj.maximum}).`);
80
+ }
81
+ }
82
+ if (typeof propObj.minimum === 'number' && val < propObj.minimum) {
83
+ errors.push(`Field "${key}" value ${val} is less than minimum ${propObj.minimum}.`);
84
+ }
85
+ if (typeof propObj.maximum === 'number' && val > propObj.maximum) {
86
+ errors.push(`Field "${key}" value ${val} is greater than maximum ${propObj.maximum}.`);
87
+ }
88
+ }
89
+ // String constraints check
90
+ if (typeof val === 'string') {
91
+ if (typeof propObj.minLength === 'number' && typeof propObj.maxLength === 'number') {
92
+ if (propObj.minLength > propObj.maxLength) {
93
+ errors.push(`Field "${key}" has contradictory bounds: minLength (${propObj.minLength}) > maxLength (${propObj.maxLength}).`);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ if (schema.$ref || schema.oneOf || schema.anyOf || schema.allOf) {
99
+ caveats.push('Schema uses complex combinators or references ($ref/oneOf/anyOf/allOf).');
100
+ }
101
+ return {
102
+ valid: errors.length === 0,
103
+ errors,
104
+ caveats,
105
+ };
106
+ }
107
+ export const sampleCallSimulationCheck = {
108
+ id: 'schema.sample-call-simulation',
109
+ description: 'Generates and validates a minimal synthetic argument object against the tool inputSchema.',
110
+ run(connection) {
111
+ const results = [];
112
+ try {
113
+ if (!connection.tools || !Array.isArray(connection.tools)) {
114
+ return results;
115
+ }
116
+ for (const tool of connection.tools) {
117
+ const schema = tool.inputSchema;
118
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
119
+ results.push({
120
+ checkId: 'schema.sample-call-simulation',
121
+ severity: 'error',
122
+ message: `Cannot simulate sample call for tool "${tool.name}": inputSchema is not a valid object.`,
123
+ serverName: connection.server.name,
124
+ toolName: tool.name,
125
+ });
126
+ continue;
127
+ }
128
+ const schemaObj = schema;
129
+ const required = Array.isArray(schemaObj.required) ? schemaObj.required : [];
130
+ const properties = schemaObj.properties &&
131
+ typeof schemaObj.properties === 'object' &&
132
+ !Array.isArray(schemaObj.properties)
133
+ ? schemaObj.properties
134
+ : {};
135
+ const syntheticArgs = {};
136
+ const generationCaveats = [];
137
+ let generationFailed = false;
138
+ for (const reqField of required) {
139
+ if (typeof reqField !== 'string')
140
+ continue;
141
+ if (!(reqField in properties)) {
142
+ results.push({
143
+ checkId: 'schema.sample-call-simulation',
144
+ severity: 'error',
145
+ message: `Sample call generation failed for tool "${tool.name}": required field "${reqField}" has no property definition.`,
146
+ serverName: connection.server.name,
147
+ toolName: tool.name,
148
+ details: { missingRequiredProperty: reqField },
149
+ suggestedFix: {
150
+ description: `Define property "${reqField}" under inputSchema.properties with a valid type.`,
151
+ },
152
+ });
153
+ generationFailed = true;
154
+ break;
155
+ }
156
+ const propDef = properties[reqField];
157
+ if (!propDef || typeof propDef !== 'object' || Array.isArray(propDef)) {
158
+ results.push({
159
+ checkId: 'schema.sample-call-simulation',
160
+ severity: 'error',
161
+ message: `Sample call generation failed for tool "${tool.name}": property definition for "${reqField}" is not an object.`,
162
+ serverName: connection.server.name,
163
+ toolName: tool.name,
164
+ suggestedFix: {
165
+ description: `Update property definition for "${reqField}" to be a valid object with a "type" field.`,
166
+ },
167
+ });
168
+ generationFailed = true;
169
+ break;
170
+ }
171
+ try {
172
+ const { value, caveat } = generatePlaceholderForType(propDef);
173
+ syntheticArgs[reqField] = value;
174
+ if (caveat)
175
+ generationCaveats.push(caveat);
176
+ }
177
+ catch (genErr) {
178
+ results.push({
179
+ checkId: 'schema.sample-call-simulation',
180
+ severity: 'error',
181
+ message: `Sample call generation failed for tool "${tool.name}" on required field "${reqField}": ${genErr instanceof Error ? genErr.message : String(genErr)}`,
182
+ serverName: connection.server.name,
183
+ toolName: tool.name,
184
+ suggestedFix: {
185
+ description: `Provide valid enum options or valid bounds for required property "${reqField}".`,
186
+ },
187
+ });
188
+ generationFailed = true;
189
+ break;
190
+ }
191
+ }
192
+ if (generationFailed) {
193
+ continue;
194
+ }
195
+ // Validate the generated payload against the schema
196
+ const outcome = validateSyntheticPayload(syntheticArgs, schemaObj);
197
+ if (!outcome.valid) {
198
+ results.push({
199
+ checkId: 'schema.sample-call-simulation',
200
+ severity: 'error',
201
+ message: `Sample call simulation validation failed for tool "${tool.name}": ${outcome.errors.join('; ')}`,
202
+ serverName: connection.server.name,
203
+ toolName: tool.name,
204
+ details: {
205
+ syntheticArgs,
206
+ errors: outcome.errors,
207
+ },
208
+ suggestedFix: {
209
+ description: 'Fix conflicting property constraints (e.g. minimum <= maximum, non-empty enums).',
210
+ },
211
+ });
212
+ }
213
+ else if (generationCaveats.length > 0 || outcome.caveats.length > 0) {
214
+ const allCaveats = [...generationCaveats, ...outcome.caveats];
215
+ results.push({
216
+ checkId: 'schema.sample-call-simulation',
217
+ severity: 'warning',
218
+ message: `Sample call simulation succeeded with caveats for tool "${tool.name}": ${allCaveats.join('; ')}`,
219
+ serverName: connection.server.name,
220
+ toolName: tool.name,
221
+ details: {
222
+ syntheticArgs,
223
+ caveats: allCaveats,
224
+ },
225
+ });
226
+ }
227
+ }
228
+ }
229
+ catch (err) {
230
+ results.push({
231
+ checkId: 'schema.sample-call-simulation',
232
+ severity: 'error',
233
+ message: `check failed internally: ${err instanceof Error ? err.message : String(err)}`,
234
+ serverName: connection.server.name,
235
+ });
236
+ }
237
+ return results;
238
+ },
239
+ };
@@ -0,0 +1,2 @@
1
+ import type { Check } from '../types.js';
2
+ export declare const typeMismatchCheck: Check;
@@ -0,0 +1,154 @@
1
+ const VALID_JSON_SCHEMA_TYPES = new Set([
2
+ 'string',
3
+ 'number',
4
+ 'integer',
5
+ 'boolean',
6
+ 'object',
7
+ 'array',
8
+ 'null',
9
+ ]);
10
+ function matchesDeclaredType(value, declaredType) {
11
+ if (value === null) {
12
+ return declaredType === 'null';
13
+ }
14
+ if (Array.isArray(value)) {
15
+ return declaredType === 'array';
16
+ }
17
+ const jsType = typeof value;
18
+ if (jsType === 'string') {
19
+ return declaredType === 'string';
20
+ }
21
+ if (jsType === 'number') {
22
+ if (declaredType === 'number')
23
+ return true;
24
+ if (declaredType === 'integer')
25
+ return Number.isInteger(value);
26
+ return false;
27
+ }
28
+ if (jsType === 'boolean') {
29
+ return declaredType === 'boolean';
30
+ }
31
+ if (jsType === 'object') {
32
+ return declaredType === 'object';
33
+ }
34
+ return false;
35
+ }
36
+ function valueMatchesTypeDefinition(value, typeDef) {
37
+ if (typeof typeDef === 'string') {
38
+ return matchesDeclaredType(value, typeDef);
39
+ }
40
+ if (Array.isArray(typeDef)) {
41
+ return typeDef.some((t) => typeof t === 'string' && matchesDeclaredType(value, t));
42
+ }
43
+ return true;
44
+ }
45
+ export const typeMismatchCheck = {
46
+ id: 'schema.type-mismatch',
47
+ description: 'Flags properties with invalid types or enum values that do not match their declared type.',
48
+ run(connection) {
49
+ const results = [];
50
+ try {
51
+ if (!connection.tools || !Array.isArray(connection.tools)) {
52
+ return results;
53
+ }
54
+ for (const tool of connection.tools) {
55
+ const schema = tool.inputSchema;
56
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
57
+ continue;
58
+ }
59
+ const schemaObj = schema;
60
+ if (!schemaObj.properties ||
61
+ typeof schemaObj.properties !== 'object' ||
62
+ Array.isArray(schemaObj.properties)) {
63
+ continue;
64
+ }
65
+ const properties = schemaObj.properties;
66
+ for (const [propName, propDef] of Object.entries(properties)) {
67
+ if (!propDef || typeof propDef !== 'object' || Array.isArray(propDef)) {
68
+ continue;
69
+ }
70
+ const propObj = propDef;
71
+ const declaredType = propObj.type;
72
+ if (declaredType !== undefined) {
73
+ if (typeof declaredType === 'string') {
74
+ if (!VALID_JSON_SCHEMA_TYPES.has(declaredType)) {
75
+ results.push({
76
+ checkId: 'schema.type-mismatch',
77
+ severity: 'warning',
78
+ message: `Property "${propName}" in tool "${tool.name}" has invalid or unrecognized type "${declaredType}".`,
79
+ serverName: connection.server.name,
80
+ toolName: tool.name,
81
+ details: { property: propName, invalidType: declaredType },
82
+ suggestedFix: {
83
+ description: `Change type of property "${propName}" to a valid JSON Schema type (e.g. "string", "number", "integer", "boolean", "object", "array", "null").`,
84
+ },
85
+ });
86
+ }
87
+ }
88
+ else if (Array.isArray(declaredType)) {
89
+ for (const item of declaredType) {
90
+ if (typeof item !== 'string' || !VALID_JSON_SCHEMA_TYPES.has(item)) {
91
+ results.push({
92
+ checkId: 'schema.type-mismatch',
93
+ severity: 'warning',
94
+ message: `Property "${propName}" in tool "${tool.name}" has invalid type union entry "${String(item)}".`,
95
+ serverName: connection.server.name,
96
+ toolName: tool.name,
97
+ details: { property: propName, invalidTypeEntry: item },
98
+ suggestedFix: {
99
+ description: `Remove or correct invalid type entry "${String(item)}" in union type for property "${propName}".`,
100
+ },
101
+ });
102
+ }
103
+ }
104
+ }
105
+ else {
106
+ results.push({
107
+ checkId: 'schema.type-mismatch',
108
+ severity: 'warning',
109
+ message: `Property "${propName}" in tool "${tool.name}" has invalid type descriptor of type ${typeof declaredType}.`,
110
+ serverName: connection.server.name,
111
+ toolName: tool.name,
112
+ details: { property: propName, typeDescriptor: declaredType },
113
+ suggestedFix: {
114
+ description: `Specify property "${propName}" type as a string (e.g. "string") or array of strings.`,
115
+ },
116
+ });
117
+ }
118
+ }
119
+ // Check enum values against declared type
120
+ if (Array.isArray(propObj.enum) && declaredType !== undefined) {
121
+ for (const enumVal of propObj.enum) {
122
+ if (!valueMatchesTypeDefinition(enumVal, declaredType)) {
123
+ results.push({
124
+ checkId: 'schema.type-mismatch',
125
+ severity: 'warning',
126
+ message: `Enum value ${JSON.stringify(enumVal)} for property "${propName}" in tool "${tool.name}" does not match declared type "${JSON.stringify(declaredType)}".`,
127
+ serverName: connection.server.name,
128
+ toolName: tool.name,
129
+ details: {
130
+ property: propName,
131
+ enumValue: enumVal,
132
+ declaredType,
133
+ },
134
+ suggestedFix: {
135
+ description: `Ensure all enum entries for property "${propName}" conform to declared type "${JSON.stringify(declaredType)}".`,
136
+ },
137
+ });
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ catch (err) {
145
+ results.push({
146
+ checkId: 'schema.type-mismatch',
147
+ severity: 'error',
148
+ message: `check failed internally: ${err instanceof Error ? err.message : String(err)}`,
149
+ serverName: connection.server.name,
150
+ });
151
+ }
152
+ return results;
153
+ },
154
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ export interface ParsedArgs {
3
+ command: 'check' | 'watch' | 'check-all' | 'diff' | 'help';
4
+ configPath?: string;
5
+ configPathB?: string;
6
+ globPattern?: string;
7
+ registryServer?: string;
8
+ policyPath?: string;
9
+ exportJunit?: string;
10
+ exportJson?: string;
11
+ snapshotPath?: string;
12
+ updateSnapshotPath?: string;
13
+ json: boolean;
14
+ timeoutMs?: number;
15
+ showFixes: boolean;
16
+ verbose: boolean;
17
+ failOn: 'error' | 'warning';
18
+ }
19
+ export declare function parseArgs(argv: string[]): ParsedArgs;
20
+ export declare function main(argv?: string[]): Promise<number>;