@mcp-layer/schema 0.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 +144 -0
- package/package.json +42 -0
- package/src/index.js +485 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arnout Kazemier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @mcp-layer/schema
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @mcp-layer/schema
|
|
9
|
+
# or
|
|
10
|
+
npm install @mcp-layer/schema
|
|
11
|
+
# or
|
|
12
|
+
yarn add @mcp-layer/schema
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { load } from '@mcp-layer/config';
|
|
19
|
+
import { connect } from '@mcp-layer/connect';
|
|
20
|
+
import { extract } from '@mcp-layer/schema';
|
|
21
|
+
|
|
22
|
+
const config = await load(undefined, process.cwd());
|
|
23
|
+
const session = await connect(config, 'demo');
|
|
24
|
+
const output = await extract(session);
|
|
25
|
+
|
|
26
|
+
console.log(output.server.info);
|
|
27
|
+
console.log(output.items[0]);
|
|
28
|
+
await session.close();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## What this package does
|
|
32
|
+
|
|
33
|
+
1) Reads the MCP server capabilities and metadata from the live client connection.
|
|
34
|
+
2) Calls MCP list endpoints (`tools/list`, `resources/list`, `resources/templates/list`, `prompts/list`) using pagination.
|
|
35
|
+
3) Normalizes everything into a unified, type-discriminated schema so generators can consume a single list.
|
|
36
|
+
4) Wraps tool input/output JSON Schemas into Zod validators backed by Ajv, while preserving the original JSON Schema.
|
|
37
|
+
5) Converts prompt arguments into a lightweight JSON Schema so prompts and tools share the same `detail.input` shape.
|
|
38
|
+
|
|
39
|
+
## Output shape (authoritative)
|
|
40
|
+
|
|
41
|
+
The package returns:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
{
|
|
45
|
+
server: {
|
|
46
|
+
info, // initialize.serverInfo
|
|
47
|
+
capabilities, // initialize.capabilities
|
|
48
|
+
instructions // initialize.instructions (optional)
|
|
49
|
+
},
|
|
50
|
+
items: [
|
|
51
|
+
{
|
|
52
|
+
type, // "tool" | "resource" | "resource-template" | "prompt"
|
|
53
|
+
name,
|
|
54
|
+
title,
|
|
55
|
+
description,
|
|
56
|
+
meta, // icons, annotations, _meta
|
|
57
|
+
detail // type-specific data
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Type discriminator:
|
|
64
|
+
- `tool`, `resource`, `resource-template`, `prompt`
|
|
65
|
+
- MCP Apps do not introduce a new `type`; tools remain `tool` and UI resources remain `resource`.
|
|
66
|
+
|
|
67
|
+
### detail: tools
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
detail: {
|
|
71
|
+
input: {
|
|
72
|
+
schema, // Zod schema (Ajv-backed)
|
|
73
|
+
json, // original JSON Schema
|
|
74
|
+
error // optional Ajv compile error message
|
|
75
|
+
},
|
|
76
|
+
output: { schema, json, error }, // only when the tool advertises outputSchema
|
|
77
|
+
ui: { resourceUri, csp, permissions } // when _meta.ui is present (MCP Apps)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### detail: resources
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
detail: {
|
|
85
|
+
uri,
|
|
86
|
+
mimeType,
|
|
87
|
+
size,
|
|
88
|
+
ui: { resourceUri, csp, permissions } // when _meta.ui is present (MCP Apps)
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### detail: resource templates
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
detail: {
|
|
96
|
+
uriTemplate,
|
|
97
|
+
mimeType
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### detail: prompts
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
detail: {
|
|
105
|
+
input: {
|
|
106
|
+
schema, // Zod schema derived from prompt arguments
|
|
107
|
+
json, // JSON Schema derived from prompt arguments
|
|
108
|
+
error // optional Ajv compile error message
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## MCP Apps support
|
|
114
|
+
|
|
115
|
+
If a server exposes MCP Apps metadata:
|
|
116
|
+
- Tools may include `_meta.ui.resourceUri` pointing to a `ui://` resource.
|
|
117
|
+
- UI resources may include `_meta.ui` with CSP and permissions.
|
|
118
|
+
|
|
119
|
+
This package normalizes those fields into `detail.ui` so generators can:
|
|
120
|
+
- Detect which tools expect UI rendering.
|
|
121
|
+
- Resolve and render the `ui://` resource.
|
|
122
|
+
- Apply CSP and permissions when hosting the UI in a webview or browser sandbox.
|
|
123
|
+
|
|
124
|
+
## Generator guidance
|
|
125
|
+
|
|
126
|
+
Suggested uses for the unified schema:
|
|
127
|
+
- CLI: Use `name`, `description`, and `detail.input` to build flags and help text.
|
|
128
|
+
- REST: Use `name` for endpoints, `detail.input.json` for request validation, and `detail.output` for response schema.
|
|
129
|
+
- UI: Use `detail.ui.resourceUri` to locate the UI and read `detail.ui.csp` / `permissions`.
|
|
130
|
+
|
|
131
|
+
## Responsibilities and lifecycle
|
|
132
|
+
|
|
133
|
+
- This package does not open or close connections. It expects a live `Session` from `@mcp-layer/connect` or `@mcp-layer/attach`.
|
|
134
|
+
- You are responsible for calling `session.close()` after extraction.
|
|
135
|
+
- If the server doesn't advertise a capability (tools/resources/prompts), extraction skips that surface.
|
|
136
|
+
|
|
137
|
+
## Error handling
|
|
138
|
+
|
|
139
|
+
- 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
|
+
- Servers that return empty lists produce an empty `items` array (not an error).
|
|
141
|
+
|
|
142
|
+
## JSON Schema vs Zod
|
|
143
|
+
|
|
144
|
+
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.
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcp-layer/schema",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Extract and normalize MCP schemas into a unified Zod-based format.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"schema",
|
|
9
|
+
"zod",
|
|
10
|
+
"tools",
|
|
11
|
+
"resources",
|
|
12
|
+
"prompts"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test --experimental-test-coverage ./test/**/*.js"
|
|
16
|
+
},
|
|
17
|
+
"main": "src/index.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": "./src/index.js",
|
|
21
|
+
"default": "./src/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"ajv": "^8.17.1",
|
|
34
|
+
"zod": "^3.25.76"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@mcp-layer/attach": "workspace:*",
|
|
38
|
+
"@mcp-layer/config": "workspace:*",
|
|
39
|
+
"@mcp-layer/connect": "workspace:*",
|
|
40
|
+
"@mcp-layer/test-server": "workspace:*"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Test if a value is a plain object.
|
|
8
|
+
* @param {unknown} value
|
|
9
|
+
* @returns {value is Record<string, unknown>}
|
|
10
|
+
*/
|
|
11
|
+
function isrecord(value) {
|
|
12
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a Zod refinement function backed by Ajv.
|
|
17
|
+
* @param {import('ajv').Ajv} validator
|
|
18
|
+
* @param {import('ajv').ValidateFunction} check
|
|
19
|
+
* @returns {(value: unknown, ctx: import('zod').RefinementCtx) => void}
|
|
20
|
+
*/
|
|
21
|
+
function refiners(validator, check) {
|
|
22
|
+
/**
|
|
23
|
+
* Validate the value and emit Zod issues for Ajv failures.
|
|
24
|
+
* @param {unknown} value
|
|
25
|
+
* @param {import('zod').RefinementCtx} ctx
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
function apply(value, ctx) {
|
|
29
|
+
const ok = check(value);
|
|
30
|
+
if (ok) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const text = validator.errorsText(check.errors, { dataVar: 'value' });
|
|
34
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: text });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return apply;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Wrap a JSON Schema into a Zod schema that delegates validation to Ajv.
|
|
42
|
+
* @param {unknown} json
|
|
43
|
+
* @returns {{ schema: import('zod').ZodTypeAny, json: Record<string, unknown> | undefined, error?: string }}
|
|
44
|
+
*/
|
|
45
|
+
function wrapschema(json) {
|
|
46
|
+
if (!isrecord(json)) {
|
|
47
|
+
return { schema: z.unknown(), json: undefined };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const check = ajv.compile(json);
|
|
52
|
+
const refine = refiners(ajv, check);
|
|
53
|
+
return { schema: z.any().superRefine(refine), json };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
return { schema: z.unknown(), json, error: message };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read all pages for a list request until pagination completes.
|
|
62
|
+
* @template T
|
|
63
|
+
* @param {(cursor?: string) => Promise<T & { nextCursor?: string }>} call
|
|
64
|
+
* @param {(result: T) => Array<unknown>} pull
|
|
65
|
+
* @returns {Promise<Array<unknown>>}
|
|
66
|
+
*/
|
|
67
|
+
async function page(call, pull) {
|
|
68
|
+
let cursor;
|
|
69
|
+
const list = [];
|
|
70
|
+
|
|
71
|
+
while (true) {
|
|
72
|
+
const result = await call(cursor);
|
|
73
|
+
const batch = pull(result);
|
|
74
|
+
if (Array.isArray(batch)) {
|
|
75
|
+
list.push(...batch);
|
|
76
|
+
}
|
|
77
|
+
cursor = result.nextCursor;
|
|
78
|
+
if (!cursor) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return list;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract tool entries from a client.
|
|
88
|
+
* @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client
|
|
89
|
+
* @returns {Promise<Array<Record<string, unknown>>>}
|
|
90
|
+
*/
|
|
91
|
+
async function tools(client) {
|
|
92
|
+
/**
|
|
93
|
+
* Fetch a page of tools.
|
|
94
|
+
* @param {string | undefined} cursor
|
|
95
|
+
* @returns {Promise<{ tools: Array<Record<string, unknown>>, nextCursor?: string }>}
|
|
96
|
+
*/
|
|
97
|
+
async function call(cursor) {
|
|
98
|
+
if (cursor) {
|
|
99
|
+
return client.listTools({ cursor });
|
|
100
|
+
}
|
|
101
|
+
return client.listTools();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pluck tool definitions from a list response.
|
|
106
|
+
* @param {{ tools?: Array<Record<string, unknown>> }} result
|
|
107
|
+
* @returns {Array<Record<string, unknown>>}
|
|
108
|
+
*/
|
|
109
|
+
function pull(result) {
|
|
110
|
+
return Array.isArray(result.tools) ? result.tools : [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return page(call, pull);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extract resource entries from a client.
|
|
118
|
+
* @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client
|
|
119
|
+
* @returns {Promise<Array<Record<string, unknown>>>}
|
|
120
|
+
*/
|
|
121
|
+
async function resources(client) {
|
|
122
|
+
/**
|
|
123
|
+
* Fetch a page of resources.
|
|
124
|
+
* @param {string | undefined} cursor
|
|
125
|
+
* @returns {Promise<{ resources: Array<Record<string, unknown>>, nextCursor?: string }>}
|
|
126
|
+
*/
|
|
127
|
+
async function call(cursor) {
|
|
128
|
+
if (cursor) {
|
|
129
|
+
return client.listResources({ cursor });
|
|
130
|
+
}
|
|
131
|
+
return client.listResources();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Pluck resource definitions from a list response.
|
|
136
|
+
* @param {{ resources?: Array<Record<string, unknown>> }} result
|
|
137
|
+
* @returns {Array<Record<string, unknown>>}
|
|
138
|
+
*/
|
|
139
|
+
function pull(result) {
|
|
140
|
+
return Array.isArray(result.resources) ? result.resources : [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return page(call, pull);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Extract resource template entries from a client.
|
|
148
|
+
* @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client
|
|
149
|
+
* @returns {Promise<Array<Record<string, unknown>>>}
|
|
150
|
+
*/
|
|
151
|
+
async function templates(client) {
|
|
152
|
+
/**
|
|
153
|
+
* Fetch a page of resource templates.
|
|
154
|
+
* @param {string | undefined} cursor
|
|
155
|
+
* @returns {Promise<{ resourceTemplates: Array<Record<string, unknown>>, nextCursor?: string }>}
|
|
156
|
+
*/
|
|
157
|
+
async function call(cursor) {
|
|
158
|
+
if (cursor) {
|
|
159
|
+
return client.listResourceTemplates({ cursor });
|
|
160
|
+
}
|
|
161
|
+
return client.listResourceTemplates();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Pluck resource template definitions from a list response.
|
|
166
|
+
* @param {{ resourceTemplates?: Array<Record<string, unknown>> }} result
|
|
167
|
+
* @returns {Array<Record<string, unknown>>}
|
|
168
|
+
*/
|
|
169
|
+
function pull(result) {
|
|
170
|
+
return Array.isArray(result.resourceTemplates) ? result.resourceTemplates : [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return page(call, pull);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extract prompt entries from a client.
|
|
178
|
+
* @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client
|
|
179
|
+
* @returns {Promise<Array<Record<string, unknown>>>}
|
|
180
|
+
*/
|
|
181
|
+
async function prompts(client) {
|
|
182
|
+
/**
|
|
183
|
+
* Fetch a page of prompts.
|
|
184
|
+
* @param {string | undefined} cursor
|
|
185
|
+
* @returns {Promise<{ prompts: Array<Record<string, unknown>>, nextCursor?: string }>}
|
|
186
|
+
*/
|
|
187
|
+
async function call(cursor) {
|
|
188
|
+
if (cursor) {
|
|
189
|
+
return client.listPrompts({ cursor });
|
|
190
|
+
}
|
|
191
|
+
return client.listPrompts();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Pluck prompt definitions from a list response.
|
|
196
|
+
* @param {{ prompts?: Array<Record<string, unknown>> }} result
|
|
197
|
+
* @returns {Array<Record<string, unknown>>}
|
|
198
|
+
*/
|
|
199
|
+
function pull(result) {
|
|
200
|
+
return Array.isArray(result.prompts) ? result.prompts : [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return page(call, pull);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build shared metadata from an MCP item.
|
|
208
|
+
* @param {Record<string, unknown>} item
|
|
209
|
+
* @returns {Record<string, unknown>}
|
|
210
|
+
*/
|
|
211
|
+
function meta(item) {
|
|
212
|
+
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
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Build metadata for tools, including annotations.
|
|
224
|
+
* @param {Record<string, unknown>} tool
|
|
225
|
+
* @returns {Record<string, unknown>}
|
|
226
|
+
*/
|
|
227
|
+
function toolmeta(tool) {
|
|
228
|
+
const out = meta(tool);
|
|
229
|
+
if (isrecord(tool.annotations)) {
|
|
230
|
+
out.annotations = tool.annotations;
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Derive a display title with tool annotation fallback.
|
|
237
|
+
* @param {Record<string, unknown>} tool
|
|
238
|
+
* @returns {string | undefined}
|
|
239
|
+
*/
|
|
240
|
+
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
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Extract MCP Apps UI metadata from an item.
|
|
252
|
+
* @param {Record<string, unknown>} item
|
|
253
|
+
* @returns {Record<string, unknown> | undefined}
|
|
254
|
+
*/
|
|
255
|
+
function uiitem(item) {
|
|
256
|
+
if (!isrecord(item._meta)) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
if (!isrecord(item._meta.ui)) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
const ui = /** @type {Record<string, unknown>} */ (item._meta.ui);
|
|
263
|
+
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
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Normalize a tool definition into the unified schema.
|
|
281
|
+
* @param {Record<string, unknown>} tool
|
|
282
|
+
* @returns {Record<string, unknown>}
|
|
283
|
+
*/
|
|
284
|
+
function toolitem(tool) {
|
|
285
|
+
const input = wrapschema(tool.inputSchema);
|
|
286
|
+
const output = wrapschema(tool.outputSchema);
|
|
287
|
+
const detail = { input };
|
|
288
|
+
const ui = uiitem(tool);
|
|
289
|
+
|
|
290
|
+
if (isrecord(tool.outputSchema)) {
|
|
291
|
+
detail.output = output;
|
|
292
|
+
}
|
|
293
|
+
if (ui) {
|
|
294
|
+
detail.ui = ui;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
type: 'tool',
|
|
299
|
+
name: typeof tool.name === 'string' ? tool.name : 'tool',
|
|
300
|
+
title: tooltitle(tool),
|
|
301
|
+
description: typeof tool.description === 'string' ? tool.description : undefined,
|
|
302
|
+
meta: toolmeta(tool),
|
|
303
|
+
detail
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Normalize a resource definition into the unified schema.
|
|
309
|
+
* @param {Record<string, unknown>} resource
|
|
310
|
+
* @returns {Record<string, unknown>}
|
|
311
|
+
*/
|
|
312
|
+
function resourceitem(resource) {
|
|
313
|
+
const ui = uiitem(resource);
|
|
314
|
+
return {
|
|
315
|
+
type: 'resource',
|
|
316
|
+
name: typeof resource.name === 'string' ? resource.name : 'resource',
|
|
317
|
+
title: typeof resource.title === 'string' ? resource.title : undefined,
|
|
318
|
+
description: typeof resource.description === 'string' ? resource.description : undefined,
|
|
319
|
+
meta: meta(resource),
|
|
320
|
+
detail: {
|
|
321
|
+
uri: typeof resource.uri === 'string' ? resource.uri : undefined,
|
|
322
|
+
mimeType: typeof resource.mimeType === 'string' ? resource.mimeType : undefined,
|
|
323
|
+
size: typeof resource.size === 'number' ? resource.size : undefined,
|
|
324
|
+
ui
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Normalize a resource template definition into the unified schema.
|
|
331
|
+
* @param {Record<string, unknown>} template
|
|
332
|
+
* @returns {Record<string, unknown>}
|
|
333
|
+
*/
|
|
334
|
+
function templateitem(template) {
|
|
335
|
+
return {
|
|
336
|
+
type: 'resource-template',
|
|
337
|
+
name: typeof template.name === 'string' ? template.name : 'resource-template',
|
|
338
|
+
title: typeof template.title === 'string' ? template.title : undefined,
|
|
339
|
+
description: typeof template.description === 'string' ? template.description : undefined,
|
|
340
|
+
meta: meta(template),
|
|
341
|
+
detail: {
|
|
342
|
+
uriTemplate: typeof template.uriTemplate === 'string' ? template.uriTemplate : undefined,
|
|
343
|
+
mimeType: typeof template.mimeType === 'string' ? template.mimeType : undefined
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Build a JSON schema from prompt arguments so prompts share the input schema shape.
|
|
350
|
+
* @param {Record<string, unknown>} prompt
|
|
351
|
+
* @returns {{ schema: import('zod').ZodTypeAny, json: Record<string, unknown> | undefined, error?: string }}
|
|
352
|
+
*/
|
|
353
|
+
function promptinput(prompt) {
|
|
354
|
+
if (!Array.isArray(prompt.arguments)) {
|
|
355
|
+
return wrapschema(undefined);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const properties = {};
|
|
359
|
+
const required = [];
|
|
360
|
+
|
|
361
|
+
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
|
+
}
|
|
368
|
+
properties[item.name] = {
|
|
369
|
+
description: typeof item.description === 'string' ? item.description : undefined
|
|
370
|
+
};
|
|
371
|
+
if (item.required === true) {
|
|
372
|
+
required.push(item.name);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const json = {
|
|
377
|
+
type: 'object',
|
|
378
|
+
properties,
|
|
379
|
+
required: required.length > 0 ? required : undefined,
|
|
380
|
+
additionalProperties: true
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
return wrapschema(json);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Normalize a prompt definition into the unified schema.
|
|
388
|
+
* @param {Record<string, unknown>} prompt
|
|
389
|
+
* @returns {Record<string, unknown>}
|
|
390
|
+
*/
|
|
391
|
+
function promptitem(prompt) {
|
|
392
|
+
const input = promptinput(prompt);
|
|
393
|
+
return {
|
|
394
|
+
type: 'prompt',
|
|
395
|
+
name: typeof prompt.name === 'string' ? prompt.name : 'prompt',
|
|
396
|
+
title: typeof prompt.title === 'string' ? prompt.title : undefined,
|
|
397
|
+
description: typeof prompt.description === 'string' ? prompt.description : undefined,
|
|
398
|
+
meta: meta(prompt),
|
|
399
|
+
detail: {
|
|
400
|
+
input
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* 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
|
|
408
|
+
* @returns {Array<Record<string, unknown>>}
|
|
409
|
+
*/
|
|
410
|
+
function normalize(data) {
|
|
411
|
+
const list = [];
|
|
412
|
+
|
|
413
|
+
if (Array.isArray(data.tools)) {
|
|
414
|
+
for (const item of data.tools) {
|
|
415
|
+
list.push(toolitem(item));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (Array.isArray(data.resources)) {
|
|
420
|
+
for (const item of data.resources) {
|
|
421
|
+
list.push(resourceitem(item));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (Array.isArray(data.templates)) {
|
|
426
|
+
for (const item of data.templates) {
|
|
427
|
+
list.push(templateitem(item));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (Array.isArray(data.prompts)) {
|
|
432
|
+
for (const item of data.prompts) {
|
|
433
|
+
list.push(promptitem(item));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return list;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Extract MCP tool/resource/prompt schemas into a unified Zod-backed format.
|
|
442
|
+
* @param {import('@mcp-layer/session').Session} link
|
|
443
|
+
* @returns {Promise<{ server: { info: Record<string, unknown> | undefined, capabilities: Record<string, unknown> | undefined, instructions: string | undefined }, items: Array<Record<string, unknown>> }>}
|
|
444
|
+
*/
|
|
445
|
+
export async function extract(link) {
|
|
446
|
+
if (!link || !link.client) {
|
|
447
|
+
throw new Error('Expected a Session instance.');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const client = link.client;
|
|
451
|
+
const caps = client.getServerCapabilities();
|
|
452
|
+
const info = client.getServerVersion();
|
|
453
|
+
const instructions = client.getInstructions();
|
|
454
|
+
|
|
455
|
+
const data = {
|
|
456
|
+
tools: undefined,
|
|
457
|
+
resources: undefined,
|
|
458
|
+
templates: undefined,
|
|
459
|
+
prompts: undefined
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
if (caps && caps.tools) {
|
|
463
|
+
data.tools = await tools(client);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (caps && caps.resources) {
|
|
467
|
+
data.resources = await resources(client);
|
|
468
|
+
data.templates = await templates(client);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (caps && caps.prompts) {
|
|
472
|
+
data.prompts = await prompts(client);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const items = normalize(data);
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
server: {
|
|
479
|
+
info,
|
|
480
|
+
capabilities: caps,
|
|
481
|
+
instructions
|
|
482
|
+
},
|
|
483
|
+
items
|
|
484
|
+
};
|
|
485
|
+
}
|