@mcp-layer/schema 1.0.0 → 1.0.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.
Files changed (3) hide show
  1. package/README.md +47 -0
  2. package/package.json +7 -6
  3. package/src/index.js +78 -99
package/README.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  `@mcp-layer/schema` extracts MCP server definitions (tools, resources, prompts, templates, and MCP Apps metadata) and normalizes them into a unified Zod-backed schema. The output is designed for downstream generators that need a single contract to build CLIs, REST endpoints, UI renderers, or additional MCP layers without re-parsing the MCP protocol surface.
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Usage](#usage)
9
+ - [What this package does](#what-this-package-does)
10
+ - [Output shape (authoritative)](#output-shape-authoritative)
11
+ - [MCP Apps support](#mcp-apps-support)
12
+ - [Generator guidance](#generator-guidance)
13
+ - [Responsibilities and lifecycle](#responsibilities-and-lifecycle)
14
+ - [Error handling](#error-handling)
15
+ - [JSON Schema vs Zod](#json-schema-vs-zod)
16
+ - [Runtime Error Reference](#runtime-error-reference)
17
+
5
18
  ## Installation
6
19
 
7
20
  ```sh
@@ -138,7 +151,41 @@ Suggested uses for the unified schema:
138
151
 
139
152
  - Schema compilation uses Ajv under the hood. If a tool exposes an invalid JSON Schema, extraction still succeeds but `detail.input.error` or `detail.output.error` will be populated for that item.
140
153
  - Servers that return empty lists produce an empty `items` array (not an error).
154
+ - Servers that support resources but not resource templates are treated as having zero templates.
141
155
 
142
156
  ## JSON Schema vs Zod
143
157
 
144
158
  Zod validators are produced to allow direct runtime validation, but the original JSON Schema is preserved in `detail.input.json` and `detail.output.json` so generators can emit OpenAPI, JSON Schema, or other schema formats without losing fidelity.
159
+
160
+ ## Runtime Error Reference
161
+
162
+ This section is written for high-pressure debugging moments. Each entry maps directly to schema extraction preconditions.
163
+
164
+ <a id="error-49ae61"></a>
165
+ ### Expected a Session instance.
166
+
167
+ Thrown from: `extract`
168
+
169
+ This happens when `extract(link)` is called without a connected `Session` object (or with an object that does not expose `.client`). The extractor relies on MCP client capabilities and list calls.
170
+
171
+ Step-by-step resolution:
172
+ 1. Verify you are passing the `Session` returned by `connect(...)` or `attach(...)`.
173
+ 2. Confirm session setup completed and `session.client` is available.
174
+ 3. Avoid passing raw MCP client objects directly; wrap flows around `Session`.
175
+ 4. Add an integration test that extracts from a real connected session.
176
+
177
+ <details>
178
+ <summary>Fix Example: extract schemas from a connected Session</summary>
179
+
180
+ ```js
181
+ const session = await connect(config, 'local-dev');
182
+ const catalog = await extract(session);
183
+ console.log(catalog.items.length);
184
+ await session.close();
185
+ ```
186
+
187
+ </details>
188
+
189
+ ## License
190
+
191
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-layer/schema",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Extract and normalize MCP schemas into a unified Zod-based format.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,13 +32,14 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "ajv": "^8.17.1",
35
- "zod": "^3.25.76"
35
+ "zod": "^3.25.76",
36
+ "@mcp-layer/error": "0.2.0"
36
37
  },
37
38
  "devDependencies": {
38
- "@mcp-layer/attach": "1.0.0",
39
- "@mcp-layer/config": "1.0.0",
40
- "@mcp-layer/connect": "1.0.0",
41
- "@mcp-layer/test-server": "1.0.0"
39
+ "@mcp-layer/attach": "1.1.1",
40
+ "@mcp-layer/config": "1.0.1",
41
+ "@mcp-layer/connect": "1.0.1",
42
+ "@mcp-layer/test-server": "1.0.1"
42
43
  },
43
44
  "scripts": {
44
45
  "test": "node --test --experimental-test-coverage ./test/**/*.js"
package/src/index.js CHANGED
@@ -1,35 +1,47 @@
1
1
  import Ajv from 'ajv';
2
2
  import { z } from 'zod';
3
+ import { LayerError } from '@mcp-layer/error';
3
4
 
4
5
  const ajv = new Ajv({ allErrors: true, strict: false });
5
6
 
6
7
  /**
7
8
  * Test if a value is a plain object.
8
- * @param {unknown} value
9
+ * @param {unknown} value - Value to test for plain object shape.
9
10
  * @returns {value is Record<string, unknown>}
10
11
  */
11
12
  function isrecord(value) {
12
13
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
13
14
  }
14
15
 
16
+ /**
17
+ * Determine whether an MCP error indicates a missing method.
18
+ * @param {unknown} error - Error value to inspect.
19
+ * @returns {boolean}
20
+ */
21
+ function ismissing(error) {
22
+ if (!error || typeof error !== 'object') return false;
23
+ const code = 'code' in error ? error.code : undefined;
24
+ if (code === -32601) return true;
25
+ const message = 'message' in error ? String(error.message) : '';
26
+ return message.includes('Method') && message.includes('not found');
27
+ }
28
+
15
29
  /**
16
30
  * Build a Zod refinement function backed by Ajv.
17
- * @param {import('ajv').Ajv} validator
18
- * @param {import('ajv').ValidateFunction} check
31
+ * @param {import('ajv').Ajv} validator - Ajv instance used to format errors.
32
+ * @param {import('ajv').ValidateFunction} check - Compiled Ajv validation function.
19
33
  * @returns {(value: unknown, ctx: import('zod').RefinementCtx) => void}
20
34
  */
21
35
  function refiners(validator, check) {
22
36
  /**
23
37
  * Validate the value and emit Zod issues for Ajv failures.
24
- * @param {unknown} value
25
- * @param {import('zod').RefinementCtx} ctx
38
+ * @param {unknown} value - Value to validate.
39
+ * @param {import('zod').RefinementCtx} ctx - Zod refinement context for reporting issues.
26
40
  * @returns {void}
27
41
  */
28
42
  function apply(value, ctx) {
29
43
  const ok = check(value);
30
- if (ok) {
31
- return;
32
- }
44
+ if (ok) return;
33
45
  const text = validator.errorsText(check.errors, { dataVar: 'value' });
34
46
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: text });
35
47
  }
@@ -39,7 +51,7 @@ function refiners(validator, check) {
39
51
 
40
52
  /**
41
53
  * Wrap a JSON Schema into a Zod schema that delegates validation to Ajv.
42
- * @param {unknown} json
54
+ * @param {unknown} json - JSON Schema definition to wrap.
43
55
  * @returns {{ schema: import('zod').ZodTypeAny, json: Record<string, unknown> | undefined, error?: string }}
44
56
  */
45
57
  function wrapschema(json) {
@@ -60,8 +72,8 @@ function wrapschema(json) {
60
72
  /**
61
73
  * Read all pages for a list request until pagination completes.
62
74
  * @template T
63
- * @param {(cursor?: string) => Promise<T & { nextCursor?: string }>} call
64
- * @param {(result: T) => Array<unknown>} pull
75
+ * @param {(cursor?: string) => Promise<T & { nextCursor?: string }>} call - Function that requests a page by cursor.
76
+ * @param {(result: T) => Array<unknown>} pull - Function that extracts items from each page result.
65
77
  * @returns {Promise<Array<unknown>>}
66
78
  */
67
79
  async function page(call, pull) {
@@ -71,13 +83,9 @@ async function page(call, pull) {
71
83
  while (true) {
72
84
  const result = await call(cursor);
73
85
  const batch = pull(result);
74
- if (Array.isArray(batch)) {
75
- list.push(...batch);
76
- }
86
+ if (Array.isArray(batch)) list.push(...batch);
77
87
  cursor = result.nextCursor;
78
- if (!cursor) {
79
- break;
80
- }
88
+ if (!cursor) break;
81
89
  }
82
90
 
83
91
  return list;
@@ -85,13 +93,13 @@ async function page(call, pull) {
85
93
 
86
94
  /**
87
95
  * Extract tool entries from a client.
88
- * @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client
96
+ * @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client - MCP client instance.
89
97
  * @returns {Promise<Array<Record<string, unknown>>>}
90
98
  */
91
99
  async function tools(client) {
92
100
  /**
93
101
  * Fetch a page of tools.
94
- * @param {string | undefined} cursor
102
+ * @param {string | undefined} cursor - Pagination cursor from the previous page.
95
103
  * @returns {Promise<{ tools: Array<Record<string, unknown>>, nextCursor?: string }>}
96
104
  */
97
105
  async function call(cursor) {
@@ -103,7 +111,7 @@ async function tools(client) {
103
111
 
104
112
  /**
105
113
  * Pluck tool definitions from a list response.
106
- * @param {{ tools?: Array<Record<string, unknown>> }} result
114
+ * @param {{ tools?: Array<Record<string, unknown>> }} result - listTools response payload.
107
115
  * @returns {Array<Record<string, unknown>>}
108
116
  */
109
117
  function pull(result) {
@@ -115,13 +123,13 @@ async function tools(client) {
115
123
 
116
124
  /**
117
125
  * Extract resource entries from a client.
118
- * @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client
126
+ * @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client - MCP client instance.
119
127
  * @returns {Promise<Array<Record<string, unknown>>>}
120
128
  */
121
129
  async function resources(client) {
122
130
  /**
123
131
  * Fetch a page of resources.
124
- * @param {string | undefined} cursor
132
+ * @param {string | undefined} cursor - Pagination cursor from the previous page.
125
133
  * @returns {Promise<{ resources: Array<Record<string, unknown>>, nextCursor?: string }>}
126
134
  */
127
135
  async function call(cursor) {
@@ -133,7 +141,7 @@ async function resources(client) {
133
141
 
134
142
  /**
135
143
  * Pluck resource definitions from a list response.
136
- * @param {{ resources?: Array<Record<string, unknown>> }} result
144
+ * @param {{ resources?: Array<Record<string, unknown>> }} result - listResources response payload.
137
145
  * @returns {Array<Record<string, unknown>>}
138
146
  */
139
147
  function pull(result) {
@@ -145,13 +153,13 @@ async function resources(client) {
145
153
 
146
154
  /**
147
155
  * Extract resource template entries from a client.
148
- * @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client
156
+ * @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client - MCP client instance.
149
157
  * @returns {Promise<Array<Record<string, unknown>>>}
150
158
  */
151
159
  async function templates(client) {
152
160
  /**
153
161
  * Fetch a page of resource templates.
154
- * @param {string | undefined} cursor
162
+ * @param {string | undefined} cursor - Pagination cursor from the previous page.
155
163
  * @returns {Promise<{ resourceTemplates: Array<Record<string, unknown>>, nextCursor?: string }>}
156
164
  */
157
165
  async function call(cursor) {
@@ -163,25 +171,30 @@ async function templates(client) {
163
171
 
164
172
  /**
165
173
  * Pluck resource template definitions from a list response.
166
- * @param {{ resourceTemplates?: Array<Record<string, unknown>> }} result
174
+ * @param {{ resourceTemplates?: Array<Record<string, unknown>> }} result - listResourceTemplates response payload.
167
175
  * @returns {Array<Record<string, unknown>>}
168
176
  */
169
177
  function pull(result) {
170
178
  return Array.isArray(result.resourceTemplates) ? result.resourceTemplates : [];
171
179
  }
172
180
 
173
- return page(call, pull);
181
+ try {
182
+ return await page(call, pull);
183
+ } catch (error) {
184
+ if (ismissing(error)) return [];
185
+ throw error;
186
+ }
174
187
  }
175
188
 
176
189
  /**
177
190
  * Extract prompt entries from a client.
178
- * @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client
191
+ * @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client - MCP client instance.
179
192
  * @returns {Promise<Array<Record<string, unknown>>>}
180
193
  */
181
194
  async function prompts(client) {
182
195
  /**
183
196
  * Fetch a page of prompts.
184
- * @param {string | undefined} cursor
197
+ * @param {string | undefined} cursor - Pagination cursor from the previous page.
185
198
  * @returns {Promise<{ prompts: Array<Record<string, unknown>>, nextCursor?: string }>}
186
199
  */
187
200
  async function call(cursor) {
@@ -193,7 +206,7 @@ async function prompts(client) {
193
206
 
194
207
  /**
195
208
  * Pluck prompt definitions from a list response.
196
- * @param {{ prompts?: Array<Record<string, unknown>> }} result
209
+ * @param {{ prompts?: Array<Record<string, unknown>> }} result - listPrompts response payload.
197
210
  * @returns {Array<Record<string, unknown>>}
198
211
  */
199
212
  function pull(result) {
@@ -205,80 +218,58 @@ async function prompts(client) {
205
218
 
206
219
  /**
207
220
  * Build shared metadata from an MCP item.
208
- * @param {Record<string, unknown>} item
221
+ * @param {Record<string, unknown>} item - MCP definition object.
209
222
  * @returns {Record<string, unknown>}
210
223
  */
211
224
  function meta(item) {
212
225
  const out = {};
213
- if (Array.isArray(item.icons)) {
214
- out.icons = item.icons;
215
- }
216
- if (isrecord(item._meta)) {
217
- out._meta = item._meta;
218
- }
226
+ if (Array.isArray(item.icons)) out.icons = item.icons;
227
+ if (isrecord(item._meta)) out._meta = item._meta;
219
228
  return out;
220
229
  }
221
230
 
222
231
  /**
223
232
  * Build metadata for tools, including annotations.
224
- * @param {Record<string, unknown>} tool
233
+ * @param {Record<string, unknown>} tool - Tool definition object.
225
234
  * @returns {Record<string, unknown>}
226
235
  */
227
236
  function toolmeta(tool) {
228
237
  const out = meta(tool);
229
- if (isrecord(tool.annotations)) {
230
- out.annotations = tool.annotations;
231
- }
238
+ if (isrecord(tool.annotations)) out.annotations = tool.annotations;
232
239
  return out;
233
240
  }
234
241
 
235
242
  /**
236
243
  * Derive a display title with tool annotation fallback.
237
- * @param {Record<string, unknown>} tool
244
+ * @param {Record<string, unknown>} tool - Tool definition object.
238
245
  * @returns {string | undefined}
239
246
  */
240
247
  function tooltitle(tool) {
241
- if (typeof tool.title === 'string') {
242
- return tool.title;
243
- }
244
- if (isrecord(tool.annotations) && typeof tool.annotations.title === 'string') {
245
- return tool.annotations.title;
246
- }
248
+ if (typeof tool.title === 'string') return tool.title;
249
+ if (isrecord(tool.annotations) && typeof tool.annotations.title === 'string') return tool.annotations.title;
247
250
  return undefined;
248
251
  }
249
252
 
250
253
  /**
251
254
  * Extract MCP Apps UI metadata from an item.
252
- * @param {Record<string, unknown>} item
255
+ * @param {Record<string, unknown>} item - MCP definition object.
253
256
  * @returns {Record<string, unknown> | undefined}
254
257
  */
255
258
  function uiitem(item) {
256
- if (!isrecord(item._meta)) {
257
- return undefined;
258
- }
259
- if (!isrecord(item._meta.ui)) {
260
- return undefined;
261
- }
259
+ if (!isrecord(item._meta)) return undefined;
260
+ if (!isrecord(item._meta.ui)) return undefined;
262
261
  const ui = /** @type {Record<string, unknown>} */ (item._meta.ui);
263
262
  const out = {};
264
- if (typeof ui.resourceUri === 'string') {
265
- out.resourceUri = ui.resourceUri;
266
- }
267
- if (typeof ui.csp === 'string') {
268
- out.csp = ui.csp;
269
- }
270
- if (Array.isArray(ui.permissions)) {
271
- out.permissions = ui.permissions;
272
- }
273
- if (Object.keys(out).length === 0) {
274
- return undefined;
275
- }
263
+ if (typeof ui.resourceUri === 'string') out.resourceUri = ui.resourceUri;
264
+ if (typeof ui.csp === 'string') out.csp = ui.csp;
265
+ if (Array.isArray(ui.permissions)) out.permissions = ui.permissions;
266
+ if (Object.keys(out).length === 0) return undefined;
276
267
  return out;
277
268
  }
278
269
 
279
270
  /**
280
271
  * Normalize a tool definition into the unified schema.
281
- * @param {Record<string, unknown>} tool
272
+ * @param {Record<string, unknown>} tool - Raw tool definition from MCP.
282
273
  * @returns {Record<string, unknown>}
283
274
  */
284
275
  function toolitem(tool) {
@@ -287,12 +278,8 @@ function toolitem(tool) {
287
278
  const detail = { input };
288
279
  const ui = uiitem(tool);
289
280
 
290
- if (isrecord(tool.outputSchema)) {
291
- detail.output = output;
292
- }
293
- if (ui) {
294
- detail.ui = ui;
295
- }
281
+ if (isrecord(tool.outputSchema)) detail.output = output;
282
+ if (ui) detail.ui = ui;
296
283
 
297
284
  return {
298
285
  type: 'tool',
@@ -306,7 +293,7 @@ function toolitem(tool) {
306
293
 
307
294
  /**
308
295
  * Normalize a resource definition into the unified schema.
309
- * @param {Record<string, unknown>} resource
296
+ * @param {Record<string, unknown>} resource - Raw resource definition from MCP.
310
297
  * @returns {Record<string, unknown>}
311
298
  */
312
299
  function resourceitem(resource) {
@@ -328,7 +315,7 @@ function resourceitem(resource) {
328
315
 
329
316
  /**
330
317
  * Normalize a resource template definition into the unified schema.
331
- * @param {Record<string, unknown>} template
318
+ * @param {Record<string, unknown>} template - Raw resource template definition from MCP.
332
319
  * @returns {Record<string, unknown>}
333
320
  */
334
321
  function templateitem(template) {
@@ -347,30 +334,22 @@ function templateitem(template) {
347
334
 
348
335
  /**
349
336
  * Build a JSON schema from prompt arguments so prompts share the input schema shape.
350
- * @param {Record<string, unknown>} prompt
337
+ * @param {Record<string, unknown>} prompt - Raw prompt definition from MCP.
351
338
  * @returns {{ schema: import('zod').ZodTypeAny, json: Record<string, unknown> | undefined, error?: string }}
352
339
  */
353
340
  function promptinput(prompt) {
354
- if (!Array.isArray(prompt.arguments)) {
355
- return wrapschema(undefined);
356
- }
341
+ if (!Array.isArray(prompt.arguments)) return wrapschema(undefined);
357
342
 
358
343
  const properties = {};
359
344
  const required = [];
360
345
 
361
346
  for (const item of prompt.arguments) {
362
- if (!isrecord(item)) {
363
- continue;
364
- }
365
- if (typeof item.name !== 'string' || item.name.length === 0) {
366
- continue;
367
- }
347
+ if (!isrecord(item)) continue;
348
+ if (typeof item.name !== 'string' || item.name.length === 0) continue;
368
349
  properties[item.name] = {
369
350
  description: typeof item.description === 'string' ? item.description : undefined
370
351
  };
371
- if (item.required === true) {
372
- required.push(item.name);
373
- }
352
+ if (item.required === true) required.push(item.name);
374
353
  }
375
354
 
376
355
  const json = {
@@ -385,7 +364,7 @@ function promptinput(prompt) {
385
364
 
386
365
  /**
387
366
  * Normalize a prompt definition into the unified schema.
388
- * @param {Record<string, unknown>} prompt
367
+ * @param {Record<string, unknown>} prompt - Raw prompt definition from MCP.
389
368
  * @returns {Record<string, unknown>}
390
369
  */
391
370
  function promptitem(prompt) {
@@ -404,7 +383,7 @@ function promptitem(prompt) {
404
383
 
405
384
  /**
406
385
  * Convert a set of MCP definitions into a unified item list.
407
- * @param {{ tools?: Array<Record<string, unknown>>, resources?: Array<Record<string, unknown>>, templates?: Array<Record<string, unknown>>, prompts?: Array<Record<string, unknown>> }} data
386
+ * @param {{ tools?: Array<Record<string, unknown>>, resources?: Array<Record<string, unknown>>, templates?: Array<Record<string, unknown>>, prompts?: Array<Record<string, unknown>> }} data - Aggregated MCP list responses.
408
387
  * @returns {Array<Record<string, unknown>>}
409
388
  */
410
389
  function normalize(data) {
@@ -439,12 +418,16 @@ function normalize(data) {
439
418
 
440
419
  /**
441
420
  * Extract MCP tool/resource/prompt schemas into a unified Zod-backed format.
442
- * @param {import('@mcp-layer/session').Session} link
421
+ * @param {import('@mcp-layer/session').Session} link - Active session with a connected MCP server.
443
422
  * @returns {Promise<{ server: { info: Record<string, unknown> | undefined, capabilities: Record<string, unknown> | undefined, instructions: string | undefined }, items: Array<Record<string, unknown>> }>}
444
423
  */
445
424
  export async function extract(link) {
446
425
  if (!link || !link.client) {
447
- throw new Error('Expected a Session instance.');
426
+ throw new LayerError({
427
+ name: 'schema',
428
+ method: 'extract',
429
+ message: 'Expected a Session instance.',
430
+ });
448
431
  }
449
432
 
450
433
  const client = link.client;
@@ -459,18 +442,14 @@ export async function extract(link) {
459
442
  prompts: undefined
460
443
  };
461
444
 
462
- if (caps && caps.tools) {
463
- data.tools = await tools(client);
464
- }
445
+ if (caps && caps.tools) data.tools = await tools(client);
465
446
 
466
447
  if (caps && caps.resources) {
467
448
  data.resources = await resources(client);
468
449
  data.templates = await templates(client);
469
450
  }
470
451
 
471
- if (caps && caps.prompts) {
472
- data.prompts = await prompts(client);
473
- }
452
+ if (caps && caps.prompts) data.prompts = await prompts(client);
474
453
 
475
454
  const items = normalize(data);
476
455