@mcp-layer/schema 1.0.1 → 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 +46 -0
  2. package/package.json +7 -6
  3. package/src/index.js +31 -76
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
@@ -143,3 +156,36 @@ Suggested uses for the unified schema:
143
156
  ## JSON Schema vs Zod
144
157
 
145
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.1",
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.1.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,5 +1,6 @@
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
 
@@ -18,13 +19,9 @@ function isrecord(value) {
18
19
  * @returns {boolean}
19
20
  */
20
21
  function ismissing(error) {
21
- if (!error || typeof error !== 'object') {
22
- return false;
23
- }
22
+ if (!error || typeof error !== 'object') return false;
24
23
  const code = 'code' in error ? error.code : undefined;
25
- if (code === -32601) {
26
- return true;
27
- }
24
+ if (code === -32601) return true;
28
25
  const message = 'message' in error ? String(error.message) : '';
29
26
  return message.includes('Method') && message.includes('not found');
30
27
  }
@@ -44,9 +41,7 @@ function refiners(validator, check) {
44
41
  */
45
42
  function apply(value, ctx) {
46
43
  const ok = check(value);
47
- if (ok) {
48
- return;
49
- }
44
+ if (ok) return;
50
45
  const text = validator.errorsText(check.errors, { dataVar: 'value' });
51
46
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: text });
52
47
  }
@@ -88,13 +83,9 @@ async function page(call, pull) {
88
83
  while (true) {
89
84
  const result = await call(cursor);
90
85
  const batch = pull(result);
91
- if (Array.isArray(batch)) {
92
- list.push(...batch);
93
- }
86
+ if (Array.isArray(batch)) list.push(...batch);
94
87
  cursor = result.nextCursor;
95
- if (!cursor) {
96
- break;
97
- }
88
+ if (!cursor) break;
98
89
  }
99
90
 
100
91
  return list;
@@ -190,9 +181,7 @@ async function templates(client) {
190
181
  try {
191
182
  return await page(call, pull);
192
183
  } catch (error) {
193
- if (ismissing(error)) {
194
- return [];
195
- }
184
+ if (ismissing(error)) return [];
196
185
  throw error;
197
186
  }
198
187
  }
@@ -234,12 +223,8 @@ async function prompts(client) {
234
223
  */
235
224
  function meta(item) {
236
225
  const out = {};
237
- if (Array.isArray(item.icons)) {
238
- out.icons = item.icons;
239
- }
240
- if (isrecord(item._meta)) {
241
- out._meta = item._meta;
242
- }
226
+ if (Array.isArray(item.icons)) out.icons = item.icons;
227
+ if (isrecord(item._meta)) out._meta = item._meta;
243
228
  return out;
244
229
  }
245
230
 
@@ -250,9 +235,7 @@ function meta(item) {
250
235
  */
251
236
  function toolmeta(tool) {
252
237
  const out = meta(tool);
253
- if (isrecord(tool.annotations)) {
254
- out.annotations = tool.annotations;
255
- }
238
+ if (isrecord(tool.annotations)) out.annotations = tool.annotations;
256
239
  return out;
257
240
  }
258
241
 
@@ -262,12 +245,8 @@ function toolmeta(tool) {
262
245
  * @returns {string | undefined}
263
246
  */
264
247
  function tooltitle(tool) {
265
- if (typeof tool.title === 'string') {
266
- return tool.title;
267
- }
268
- if (isrecord(tool.annotations) && typeof tool.annotations.title === 'string') {
269
- return tool.annotations.title;
270
- }
248
+ if (typeof tool.title === 'string') return tool.title;
249
+ if (isrecord(tool.annotations) && typeof tool.annotations.title === 'string') return tool.annotations.title;
271
250
  return undefined;
272
251
  }
273
252
 
@@ -277,26 +256,14 @@ function tooltitle(tool) {
277
256
  * @returns {Record<string, unknown> | undefined}
278
257
  */
279
258
  function uiitem(item) {
280
- if (!isrecord(item._meta)) {
281
- return undefined;
282
- }
283
- if (!isrecord(item._meta.ui)) {
284
- return undefined;
285
- }
259
+ if (!isrecord(item._meta)) return undefined;
260
+ if (!isrecord(item._meta.ui)) return undefined;
286
261
  const ui = /** @type {Record<string, unknown>} */ (item._meta.ui);
287
262
  const out = {};
288
- if (typeof ui.resourceUri === 'string') {
289
- out.resourceUri = ui.resourceUri;
290
- }
291
- if (typeof ui.csp === 'string') {
292
- out.csp = ui.csp;
293
- }
294
- if (Array.isArray(ui.permissions)) {
295
- out.permissions = ui.permissions;
296
- }
297
- if (Object.keys(out).length === 0) {
298
- return undefined;
299
- }
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;
300
267
  return out;
301
268
  }
302
269
 
@@ -311,12 +278,8 @@ function toolitem(tool) {
311
278
  const detail = { input };
312
279
  const ui = uiitem(tool);
313
280
 
314
- if (isrecord(tool.outputSchema)) {
315
- detail.output = output;
316
- }
317
- if (ui) {
318
- detail.ui = ui;
319
- }
281
+ if (isrecord(tool.outputSchema)) detail.output = output;
282
+ if (ui) detail.ui = ui;
320
283
 
321
284
  return {
322
285
  type: 'tool',
@@ -375,26 +338,18 @@ function templateitem(template) {
375
338
  * @returns {{ schema: import('zod').ZodTypeAny, json: Record<string, unknown> | undefined, error?: string }}
376
339
  */
377
340
  function promptinput(prompt) {
378
- if (!Array.isArray(prompt.arguments)) {
379
- return wrapschema(undefined);
380
- }
341
+ if (!Array.isArray(prompt.arguments)) return wrapschema(undefined);
381
342
 
382
343
  const properties = {};
383
344
  const required = [];
384
345
 
385
346
  for (const item of prompt.arguments) {
386
- if (!isrecord(item)) {
387
- continue;
388
- }
389
- if (typeof item.name !== 'string' || item.name.length === 0) {
390
- continue;
391
- }
347
+ if (!isrecord(item)) continue;
348
+ if (typeof item.name !== 'string' || item.name.length === 0) continue;
392
349
  properties[item.name] = {
393
350
  description: typeof item.description === 'string' ? item.description : undefined
394
351
  };
395
- if (item.required === true) {
396
- required.push(item.name);
397
- }
352
+ if (item.required === true) required.push(item.name);
398
353
  }
399
354
 
400
355
  const json = {
@@ -468,7 +423,11 @@ function normalize(data) {
468
423
  */
469
424
  export async function extract(link) {
470
425
  if (!link || !link.client) {
471
- throw new Error('Expected a Session instance.');
426
+ throw new LayerError({
427
+ name: 'schema',
428
+ method: 'extract',
429
+ message: 'Expected a Session instance.',
430
+ });
472
431
  }
473
432
 
474
433
  const client = link.client;
@@ -483,18 +442,14 @@ export async function extract(link) {
483
442
  prompts: undefined
484
443
  };
485
444
 
486
- if (caps && caps.tools) {
487
- data.tools = await tools(client);
488
- }
445
+ if (caps && caps.tools) data.tools = await tools(client);
489
446
 
490
447
  if (caps && caps.resources) {
491
448
  data.resources = await resources(client);
492
449
  data.templates = await templates(client);
493
450
  }
494
451
 
495
- if (caps && caps.prompts) {
496
- data.prompts = await prompts(client);
497
- }
452
+ if (caps && caps.prompts) data.prompts = await prompts(client);
498
453
 
499
454
  const items = normalize(data);
500
455