@sammorrowdrums/mcpi-ext 0.2.1 → 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 +137 -47
- package/dist/code-mode/eligibility.d.ts +29 -4
- package/dist/code-mode/eligibility.js +56 -6
- package/dist/code-mode/executor.d.ts +19 -1
- package/dist/code-mode/executor.js +73 -8
- package/dist/code-mode/index.d.ts +54 -12
- package/dist/code-mode/index.js +157 -65
- package/dist/code-mode/isolated-vm.d.ts +65 -0
- package/dist/code-mode/isolated-vm.js +116 -0
- package/dist/code-mode/tools.d.ts +4 -1
- package/dist/code-mode/tools.js +7 -4
- package/dist/code-mode/type-hints.d.ts +2 -2
- package/dist/code-mode/type-hints.js +24 -8
- package/dist/docker-e2e.d.ts +0 -1
- package/dist/docker-e2e.js +0 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +146 -67
- package/dist/mcp/call-tool-result.d.ts +10 -0
- package/dist/mcp/call-tool-result.js +70 -0
- package/dist/mcp/client-factory.d.ts +59 -0
- package/dist/mcp/client-factory.js +76 -0
- package/dist/mcp/client-manager.d.ts +61 -19
- package/dist/mcp/client-manager.js +223 -64
- package/dist/mcp/config-loader.d.ts +0 -1
- package/dist/mcp/config-loader.js +0 -1
- package/dist/mcp/config.d.ts +11 -1
- package/dist/mcp/config.js +15 -1
- package/dist/mcp/gateway-defaults.d.ts +14 -0
- package/dist/mcp/gateway-defaults.js +21 -0
- package/dist/mcp/host-approval.d.ts +16 -0
- package/dist/mcp/host-approval.js +19 -0
- package/dist/mcp/host-elicitation.d.ts +19 -0
- package/dist/mcp/host-elicitation.js +94 -0
- package/dist/mcp/index.d.ts +4 -1
- package/dist/mcp/index.js +4 -1
- package/dist/mcp/policy.d.ts +314 -0
- package/dist/mcp/policy.js +848 -0
- package/dist/routing/facilities.d.ts +109 -0
- package/dist/routing/facilities.js +175 -0
- package/dist/routing/format.d.ts +24 -0
- package/dist/routing/format.js +64 -0
- package/dist/routing/index.d.ts +4 -0
- package/dist/routing/index.js +4 -0
- package/dist/routing/seam.d.ts +35 -0
- package/dist/routing/seam.js +28 -0
- package/dist/routing/tripwire.d.ts +42 -0
- package/dist/routing/tripwire.js +80 -0
- package/dist/skills/discover.d.ts +5 -3
- package/dist/skills/discover.js +37 -12
- package/dist/skills/format.d.ts +4 -1
- package/dist/skills/format.js +8 -4
- package/dist/skills/index.d.ts +2 -2
- package/dist/skills/index.js +1 -1
- package/dist/skills/load-skill-tool.d.ts +23 -7
- package/dist/skills/load-skill-tool.js +112 -33
- package/dist/skills/mcp-tool-proxy.d.ts +4 -8
- package/dist/skills/mcp-tool-proxy.js +22 -97
- package/dist/skills/sep2640/client.d.ts +91 -0
- package/dist/skills/sep2640/client.js +160 -0
- package/dist/skills/sep2640/discover.d.ts +33 -0
- package/dist/skills/sep2640/discover.js +82 -0
- package/dist/skills/sep2640/index.d.ts +13 -0
- package/dist/skills/sep2640/index.js +13 -0
- package/dist/skills/sep2640/integrity.d.ts +58 -0
- package/dist/skills/sep2640/integrity.js +185 -0
- package/dist/skills/sep2640/load.d.ts +61 -0
- package/dist/skills/sep2640/load.js +90 -0
- package/dist/skills/sep2640/protocol.d.ts +146 -0
- package/dist/skills/sep2640/protocol.js +214 -0
- package/dist/skills/sep2640/spec.d.ts +76 -0
- package/dist/skills/sep2640/spec.js +88 -0
- package/dist/skills/skill-registry.d.ts +48 -2
- package/dist/skills/skill-registry.js +40 -3
- package/dist/tool-cli/bridge.d.ts +14 -0
- package/dist/tool-cli/bridge.js +196 -0
- package/dist/tool-cli/format.d.ts +17 -5
- package/dist/tool-cli/format.js +36 -20
- package/dist/tool-cli/index.d.ts +5 -2
- package/dist/tool-cli/index.js +3 -1
- package/dist/tool-cli/provider.d.ts +21 -0
- package/dist/tool-cli/provider.js +114 -0
- package/dist/tool-cli/startup.d.ts +22 -0
- package/dist/tool-cli/startup.js +47 -0
- package/package.json +78 -30
- package/dist/code-mode/code-mode-integration.test.d.ts +0 -2
- package/dist/code-mode/code-mode-integration.test.d.ts.map +0 -1
- package/dist/code-mode/code-mode-integration.test.js +0 -116
- package/dist/code-mode/code-mode-integration.test.js.map +0 -1
- package/dist/code-mode/eligibility.d.ts.map +0 -1
- package/dist/code-mode/eligibility.js.map +0 -1
- package/dist/code-mode/eligibility.test.d.ts +0 -2
- package/dist/code-mode/eligibility.test.d.ts.map +0 -1
- package/dist/code-mode/eligibility.test.js +0 -53
- package/dist/code-mode/eligibility.test.js.map +0 -1
- package/dist/code-mode/executor.d.ts.map +0 -1
- package/dist/code-mode/executor.js.map +0 -1
- package/dist/code-mode/executor.test.d.ts +0 -2
- package/dist/code-mode/executor.test.d.ts.map +0 -1
- package/dist/code-mode/executor.test.js +0 -130
- package/dist/code-mode/executor.test.js.map +0 -1
- package/dist/code-mode/index.d.ts.map +0 -1
- package/dist/code-mode/index.js.map +0 -1
- package/dist/code-mode/tools.d.ts.map +0 -1
- package/dist/code-mode/tools.js.map +0 -1
- package/dist/code-mode/type-hints.d.ts.map +0 -1
- package/dist/code-mode/type-hints.js.map +0 -1
- package/dist/code-mode/type-hints.test.d.ts +0 -2
- package/dist/code-mode/type-hints.test.d.ts.map +0 -1
- package/dist/code-mode/type-hints.test.js +0 -157
- package/dist/code-mode/type-hints.test.js.map +0 -1
- package/dist/docker-e2e.d.ts.map +0 -1
- package/dist/docker-e2e.js.map +0 -1
- package/dist/docker-e2e.test.d.ts +0 -2
- package/dist/docker-e2e.test.d.ts.map +0 -1
- package/dist/docker-e2e.test.js +0 -176
- package/dist/docker-e2e.test.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -8
- package/dist/index.test.js.map +0 -1
- package/dist/mcp/client-manager.d.ts.map +0 -1
- package/dist/mcp/client-manager.js.map +0 -1
- package/dist/mcp/client-manager.test.d.ts +0 -2
- package/dist/mcp/client-manager.test.d.ts.map +0 -1
- package/dist/mcp/client-manager.test.js +0 -204
- package/dist/mcp/client-manager.test.js.map +0 -1
- package/dist/mcp/config-loader.d.ts.map +0 -1
- package/dist/mcp/config-loader.js.map +0 -1
- package/dist/mcp/config-loader.test.d.ts +0 -2
- package/dist/mcp/config-loader.test.d.ts.map +0 -1
- package/dist/mcp/config-loader.test.js +0 -93
- package/dist/mcp/config-loader.test.js.map +0 -1
- package/dist/mcp/config.d.ts.map +0 -1
- package/dist/mcp/config.js.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js.map +0 -1
- package/dist/skills/discover.d.ts.map +0 -1
- package/dist/skills/discover.js.map +0 -1
- package/dist/skills/format.d.ts.map +0 -1
- package/dist/skills/format.js.map +0 -1
- package/dist/skills/format.test.d.ts +0 -2
- package/dist/skills/format.test.d.ts.map +0 -1
- package/dist/skills/format.test.js +0 -63
- package/dist/skills/format.test.js.map +0 -1
- package/dist/skills/index.d.ts.map +0 -1
- package/dist/skills/index.js.map +0 -1
- package/dist/skills/load-skill-tool.d.ts.map +0 -1
- package/dist/skills/load-skill-tool.js.map +0 -1
- package/dist/skills/mcp-tool-proxy.d.ts.map +0 -1
- package/dist/skills/mcp-tool-proxy.js.map +0 -1
- package/dist/skills/skill-integration.test.d.ts +0 -2
- package/dist/skills/skill-integration.test.d.ts.map +0 -1
- package/dist/skills/skill-integration.test.js +0 -119
- package/dist/skills/skill-integration.test.js.map +0 -1
- package/dist/skills/skill-registry.d.ts.map +0 -1
- package/dist/skills/skill-registry.js.map +0 -1
- package/dist/skills/skill-registry.test.d.ts +0 -2
- package/dist/skills/skill-registry.test.d.ts.map +0 -1
- package/dist/skills/skill-registry.test.js +0 -67
- package/dist/skills/skill-registry.test.js.map +0 -1
- package/dist/test-servers/weather-server.d.ts +0 -12
- package/dist/test-servers/weather-server.d.ts.map +0 -1
- package/dist/test-servers/weather-server.js +0 -139
- package/dist/test-servers/weather-server.js.map +0 -1
- package/dist/test-servers/weather-stdio.d.ts +0 -2
- package/dist/test-servers/weather-stdio.d.ts.map +0 -1
- package/dist/test-servers/weather-stdio.js +0 -11
- package/dist/test-servers/weather-stdio.js.map +0 -1
- package/dist/tool-cli/cli.d.ts +0 -3
- package/dist/tool-cli/cli.d.ts.map +0 -1
- package/dist/tool-cli/cli.js +0 -179
- package/dist/tool-cli/cli.js.map +0 -1
- package/dist/tool-cli/constants.d.ts +0 -7
- package/dist/tool-cli/constants.d.ts.map +0 -1
- package/dist/tool-cli/constants.js +0 -15
- package/dist/tool-cli/constants.js.map +0 -1
- package/dist/tool-cli/format.d.ts.map +0 -1
- package/dist/tool-cli/format.js.map +0 -1
- package/dist/tool-cli/format.test.d.ts +0 -2
- package/dist/tool-cli/format.test.d.ts.map +0 -1
- package/dist/tool-cli/format.test.js +0 -30
- package/dist/tool-cli/format.test.js.map +0 -1
- package/dist/tool-cli/index.d.ts.map +0 -1
- package/dist/tool-cli/index.js.map +0 -1
- package/dist/tool-cli/rpc-client.d.ts +0 -6
- package/dist/tool-cli/rpc-client.d.ts.map +0 -1
- package/dist/tool-cli/rpc-client.js +0 -25
- package/dist/tool-cli/rpc-client.js.map +0 -1
- package/dist/tool-cli/rpc-server.d.ts +0 -48
- package/dist/tool-cli/rpc-server.d.ts.map +0 -1
- package/dist/tool-cli/rpc-server.js +0 -220
- package/dist/tool-cli/rpc-server.js.map +0 -1
- package/dist/tool-cli/rpc-server.test.d.ts +0 -2
- package/dist/tool-cli/rpc-server.test.d.ts.map +0 -1
- package/dist/tool-cli/rpc-server.test.js +0 -154
- package/dist/tool-cli/rpc-server.test.js.map +0 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { SkillValidationError, validateSkillEntry, } from "./protocol.js";
|
|
2
|
+
import { HONOURED_CACHE_SCOPES, MAX_CACHE_TTL_MS, MAX_SKILL_LIST_PAGES } from "./spec.js";
|
|
3
|
+
/**
|
|
4
|
+
* A client for the draft SEP-2640 skills extension.
|
|
5
|
+
*
|
|
6
|
+
* Every request goes through {@link McpPolicy}, which re-checks the negotiated
|
|
7
|
+
* capability immediately before dispatch. This class therefore never caches
|
|
8
|
+
* "server X supports skills" — it caches only listing *content*, and only when
|
|
9
|
+
* the server asked it to with terms this host is willing to honour.
|
|
10
|
+
*/
|
|
11
|
+
export class SkillsExtensionClient {
|
|
12
|
+
policy;
|
|
13
|
+
now;
|
|
14
|
+
listCache = new Map();
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.policy = options.policy;
|
|
17
|
+
this.now = options.now ?? (() => Date.now());
|
|
18
|
+
}
|
|
19
|
+
/** True when the server declared the extension on the live connection. */
|
|
20
|
+
supports(serverName) {
|
|
21
|
+
return this.policy.getSkillsExtension(serverName) !== undefined;
|
|
22
|
+
}
|
|
23
|
+
/** True when the server additionally declared `directoryRead: true`. */
|
|
24
|
+
supportsDirectoryRead(serverName) {
|
|
25
|
+
return this.policy.supportsSkillDirectoryRead(serverName);
|
|
26
|
+
}
|
|
27
|
+
/** The declared extension settings, for diagnostics. */
|
|
28
|
+
capability(serverName) {
|
|
29
|
+
return this.policy.getSkillsExtension(serverName);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Walk `skills/list` to completion, validating each entry before it is
|
|
33
|
+
* admitted.
|
|
34
|
+
*
|
|
35
|
+
* Pagination stops at {@link MAX_SKILL_LIST_PAGES}; the result is then marked
|
|
36
|
+
* `truncated` rather than presented as the whole set. A server that returns
|
|
37
|
+
* the same cursor twice is also treated as truncated, which stops a cursor
|
|
38
|
+
* loop from becoming an unbounded request stream.
|
|
39
|
+
*/
|
|
40
|
+
async listSkills(serverName, signal) {
|
|
41
|
+
const cached = this.readCache(serverName);
|
|
42
|
+
if (cached)
|
|
43
|
+
return cached;
|
|
44
|
+
const skills = [];
|
|
45
|
+
const rejected = [];
|
|
46
|
+
const seenCursors = new Set();
|
|
47
|
+
let cursor;
|
|
48
|
+
let pages = 0;
|
|
49
|
+
let truncated = false;
|
|
50
|
+
let ttlMs;
|
|
51
|
+
let cacheScope;
|
|
52
|
+
for (;;) {
|
|
53
|
+
const result = await this.policy.listMcpSkills(serverName, cursor, signal);
|
|
54
|
+
pages += 1;
|
|
55
|
+
for (const entry of result.skills) {
|
|
56
|
+
try {
|
|
57
|
+
validateSkillEntry(entry);
|
|
58
|
+
skills.push(entry);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (!(error instanceof SkillValidationError))
|
|
62
|
+
throw error;
|
|
63
|
+
rejected.push({ uri: entry.uri, reason: error.message });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Freshness hints from the first page govern the whole listing; a later
|
|
67
|
+
// page cannot extend the lifetime of data already collected.
|
|
68
|
+
if (pages === 1) {
|
|
69
|
+
ttlMs = result.ttlMs;
|
|
70
|
+
cacheScope = result.cacheScope;
|
|
71
|
+
}
|
|
72
|
+
const next = result.nextCursor;
|
|
73
|
+
if (!next)
|
|
74
|
+
break;
|
|
75
|
+
if (seenCursors.has(next) || pages >= MAX_SKILL_LIST_PAGES) {
|
|
76
|
+
truncated = true;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
seenCursors.add(next);
|
|
80
|
+
cursor = next;
|
|
81
|
+
}
|
|
82
|
+
const listing = { skills, rejected, truncated, fromCache: false };
|
|
83
|
+
this.writeCache(serverName, listing, ttlMs, cacheScope);
|
|
84
|
+
return listing;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Fetch one skill's authoritative entry with `skills/get`.
|
|
88
|
+
*
|
|
89
|
+
* Always goes to the server: `skills/get` is what re-establishes current
|
|
90
|
+
* digests after a verification failure, so serving it from cache would defeat
|
|
91
|
+
* the recovery path the spec defines.
|
|
92
|
+
*/
|
|
93
|
+
async getSkill(serverName, uri, signal) {
|
|
94
|
+
const result = await this.policy.getMcpSkill(serverName, uri, signal);
|
|
95
|
+
validateSkillEntry(result.skill);
|
|
96
|
+
return result.skill;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Enumerate a skill directory, when the server declared `directoryRead`.
|
|
100
|
+
*
|
|
101
|
+
* Directory listings are navigational only. Nothing they return becomes
|
|
102
|
+
* readable: a file is readable because the skill entry lists it with a
|
|
103
|
+
* digest, and a directory cannot add entries to that list.
|
|
104
|
+
*/
|
|
105
|
+
async readDirectory(serverName, uri, signal) {
|
|
106
|
+
const resources = [];
|
|
107
|
+
const seenCursors = new Set();
|
|
108
|
+
let cursor;
|
|
109
|
+
let pages = 0;
|
|
110
|
+
for (;;) {
|
|
111
|
+
const result = await this.policy.readSkillDirectory(serverName, uri, cursor, signal);
|
|
112
|
+
pages += 1;
|
|
113
|
+
resources.push(...result.resources);
|
|
114
|
+
const next = result.nextCursor;
|
|
115
|
+
if (!next || seenCursors.has(next) || pages >= MAX_SKILL_LIST_PAGES)
|
|
116
|
+
break;
|
|
117
|
+
seenCursors.add(next);
|
|
118
|
+
cursor = next;
|
|
119
|
+
}
|
|
120
|
+
return resources;
|
|
121
|
+
}
|
|
122
|
+
/** Drop cached listings; call on disconnect or reset. */
|
|
123
|
+
clearCache(serverName) {
|
|
124
|
+
if (serverName === undefined) {
|
|
125
|
+
this.listCache.clear();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.listCache.delete(serverName);
|
|
129
|
+
}
|
|
130
|
+
readCache(serverName) {
|
|
131
|
+
const entry = this.listCache.get(serverName);
|
|
132
|
+
if (!entry)
|
|
133
|
+
return undefined;
|
|
134
|
+
if (entry.expiresAt <= this.now()) {
|
|
135
|
+
this.listCache.delete(serverName);
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
return { ...entry.listing, fromCache: true };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Cache a listing only when the server asked for it in terms this host
|
|
142
|
+
* understands.
|
|
143
|
+
*
|
|
144
|
+
* `cacheScope`'s value set is defined by SEP-2549, not SEP-2640, so an
|
|
145
|
+
* unrecognised scope is treated as "do not cache" rather than guessed at. A
|
|
146
|
+
* truncated listing is never cached, because caching an incomplete answer is
|
|
147
|
+
* exactly the "empty listing means no skills" mistake the spec warns against,
|
|
148
|
+
* just with a longer lifetime.
|
|
149
|
+
*/
|
|
150
|
+
writeCache(serverName, listing, ttlMs, cacheScope) {
|
|
151
|
+
if (listing.truncated)
|
|
152
|
+
return;
|
|
153
|
+
if (typeof ttlMs !== "number" || !Number.isFinite(ttlMs) || ttlMs <= 0)
|
|
154
|
+
return;
|
|
155
|
+
if (cacheScope === undefined || !HONOURED_CACHE_SCOPES.has(cacheScope))
|
|
156
|
+
return;
|
|
157
|
+
const lifetime = Math.min(ttlMs, MAX_CACHE_TTL_MS);
|
|
158
|
+
this.listCache.set(serverName, { listing, expiresAt: this.now() + lifetime });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { McpPolicy } from "../../mcp/policy.js";
|
|
2
|
+
import type { McpSkillMetadata } from "../skill-registry.js";
|
|
3
|
+
import type { SkillsExtensionClient } from "./client.js";
|
|
4
|
+
/** Outcome of a SEP-2640 discovery pass against one server. */
|
|
5
|
+
export interface Sep2640DiscoveryResult {
|
|
6
|
+
readonly skills: McpSkillMetadata[];
|
|
7
|
+
/** Entries the server offered that this host refused, with the reason. */
|
|
8
|
+
readonly rejected: {
|
|
9
|
+
readonly uri: string;
|
|
10
|
+
readonly reason: string;
|
|
11
|
+
}[];
|
|
12
|
+
/** True when the listing was cut short and is known to be incomplete. */
|
|
13
|
+
readonly truncated: boolean;
|
|
14
|
+
/** True when the listing was served from the freshness cache. */
|
|
15
|
+
readonly fromCache: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Discover skills from a server that declared the draft skills extension.
|
|
19
|
+
*
|
|
20
|
+
* This reads nothing. `skills/list` already carries each skill's frontmatter
|
|
21
|
+
* verbatim, so name, description and gated tools are all available without
|
|
22
|
+
* touching SKILL.md — which is precisely what lets this honour the spec's ban
|
|
23
|
+
* on prefetching. The first byte of skill content is fetched at load time, and
|
|
24
|
+
* only then.
|
|
25
|
+
*
|
|
26
|
+
* Each accepted entry's resource set is registered with the policy, which is
|
|
27
|
+
* what later authorises reads: a file is readable because this entry listed it,
|
|
28
|
+
* not because it happens to share a URI prefix.
|
|
29
|
+
*
|
|
30
|
+
* An empty result means "the server listed nothing right now". It is not proof
|
|
31
|
+
* that the server has no skills, and callers must not cache it as such.
|
|
32
|
+
*/
|
|
33
|
+
export declare function discoverSkillsViaExtension(policy: McpPolicy, client: SkillsExtensionClient, serverName: string, log?: (msg: string) => void, signal?: AbortSignal): Promise<Sep2640DiscoveryResult>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { frontmatterDescription, frontmatterName, resourceSetFingerprint, } from "./protocol.js";
|
|
2
|
+
/**
|
|
3
|
+
* Discover skills from a server that declared the draft skills extension.
|
|
4
|
+
*
|
|
5
|
+
* This reads nothing. `skills/list` already carries each skill's frontmatter
|
|
6
|
+
* verbatim, so name, description and gated tools are all available without
|
|
7
|
+
* touching SKILL.md — which is precisely what lets this honour the spec's ban
|
|
8
|
+
* on prefetching. The first byte of skill content is fetched at load time, and
|
|
9
|
+
* only then.
|
|
10
|
+
*
|
|
11
|
+
* Each accepted entry's resource set is registered with the policy, which is
|
|
12
|
+
* what later authorises reads: a file is readable because this entry listed it,
|
|
13
|
+
* not because it happens to share a URI prefix.
|
|
14
|
+
*
|
|
15
|
+
* An empty result means "the server listed nothing right now". It is not proof
|
|
16
|
+
* that the server has no skills, and callers must not cache it as such.
|
|
17
|
+
*/
|
|
18
|
+
export async function discoverSkillsViaExtension(policy, client, serverName, log = console.error, signal) {
|
|
19
|
+
const listing = await client.listSkills(serverName, signal);
|
|
20
|
+
const skills = [];
|
|
21
|
+
const rejected = [...listing.rejected];
|
|
22
|
+
for (const entry of listing.skills) {
|
|
23
|
+
const name = frontmatterName(entry);
|
|
24
|
+
if (!name) {
|
|
25
|
+
rejected.push({ uri: entry.uri, reason: "frontmatter is missing a name" });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const fingerprint = resourceSetFingerprint(entry);
|
|
29
|
+
policy.registerSkillResources(serverName, entry.uri, listedResourceUris(entry));
|
|
30
|
+
skills.push({
|
|
31
|
+
name,
|
|
32
|
+
description: frontmatterDescription(entry) ?? "",
|
|
33
|
+
uri: entry.uri,
|
|
34
|
+
serverName,
|
|
35
|
+
allowedTools: parseAllowedTools(entry.frontmatter),
|
|
36
|
+
origin: "sep2640",
|
|
37
|
+
contentFingerprint: fingerprint,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
for (const failure of rejected) {
|
|
41
|
+
log(`[skills] Rejected skill ${failure.uri} from "${serverName}": ${failure.reason}`);
|
|
42
|
+
}
|
|
43
|
+
if (listing.truncated) {
|
|
44
|
+
log(`[skills] Listing from "${serverName}" was truncated; this is a partial view, not the full skill set`);
|
|
45
|
+
}
|
|
46
|
+
skills.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
47
|
+
return { skills, rejected, truncated: listing.truncated, fromCache: listing.fromCache };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* URIs this entry authorises for reading.
|
|
51
|
+
*
|
|
52
|
+
* A `"dynamic"` resource set authorises nothing up front — the policy's
|
|
53
|
+
* allowlist stays empty and reads are refused. Dynamic skills are readable only
|
|
54
|
+
* through an explicit per-read decision, which is the honest reading of a
|
|
55
|
+
* server declining to say what its skill contains.
|
|
56
|
+
*/
|
|
57
|
+
function listedResourceUris(entry) {
|
|
58
|
+
if (entry.resources === "dynamic")
|
|
59
|
+
return [];
|
|
60
|
+
return entry.resources.map((ref) => ref.uri);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read `allowed-tools` from verbatim SEP-2640 frontmatter.
|
|
64
|
+
*
|
|
65
|
+
* Only the spec-defined `allowed-tools` key is honoured here. The extension
|
|
66
|
+
* reserves the `io.modelcontextprotocol/` metadata prefix but defines no keys
|
|
67
|
+
* under it, so the legacy path's `io.modelcontextprotocol/tools` lookup is not
|
|
68
|
+
* repeated on this contract.
|
|
69
|
+
*
|
|
70
|
+
* These names stay inert until the user approves the grant: parsing them is not
|
|
71
|
+
* activating them.
|
|
72
|
+
*/
|
|
73
|
+
function parseAllowedTools(frontmatter) {
|
|
74
|
+
const declared = frontmatter["allowed-tools"];
|
|
75
|
+
if (Array.isArray(declared)) {
|
|
76
|
+
return declared.filter((value) => typeof value === "string");
|
|
77
|
+
}
|
|
78
|
+
if (typeof declared === "string" && declared.trim().length > 0) {
|
|
79
|
+
return declared.trim().split(/\s+/);
|
|
80
|
+
}
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the **draft** MCP skills extension, SEP-2640.
|
|
3
|
+
*
|
|
4
|
+
* This is an unratified proposal. See {@link ./spec.ts} for the exact revision
|
|
5
|
+
* this code was written against, and `docs/skills.md` for the negotiation,
|
|
6
|
+
* integrity and fallback model.
|
|
7
|
+
*/
|
|
8
|
+
export { DIRECTORY_MIME_TYPE, HONOURED_CACHE_SCOPES, MAX_CACHE_TTL_MS, MAX_SKILL_LIST_PAGES, MAX_SKILL_RESOURCE_ENTRIES, MAX_SKILL_TOTAL_BYTES, SKILL_DIGEST_PATTERN, SKILLS_EXTENSION_NAME, SKILLS_EXTENSION_REVISION, SKILLS_EXTENSION_STATUS, SKILLS_METHODS, describeNegotiation, skillsExtensionDiagnostic, } from "./spec.js";
|
|
9
|
+
export { declaredTotalBytes, DirectoryReadResultSchema, DirectoryResourceSchema, finalPathSegment, findResourceRef, frontmatterDescription, frontmatterName, isDirectoryResource, isValidDigest, resourceSetFingerprint, SkillEntrySchema, SkillResourceRefSchema, SkillsGetResultSchema, SkillsListResultSchema, skillPathOf, SkillValidationError, validateSkillEntry, type DirectoryReadResult, type DirectoryResource, type SkillEntry, type SkillResourceRef, type SkillsGetResult, type SkillsListResult, type SkillValidationCode, } from "./protocol.js";
|
|
10
|
+
export { base64ToBytes, computeDigest, SkillIntegrityError, textToBytes, verifyBytes, verifyFrontmatter, verifyNamePath, verifyResourceRead, type IntegrityFailureCode, } from "./integrity.js";
|
|
11
|
+
export { SkillsExtensionClient, type SkillsExtensionClientOptions, type SkillsListing, } from "./client.js";
|
|
12
|
+
export { discoverSkillsViaExtension, type Sep2640DiscoveryResult } from "./discover.js";
|
|
13
|
+
export { loadSkillDocument, readSkillResource, SkillFetchBudget, type ReadSkillResourceOptions, type VerifiedResource, } from "./load.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the **draft** MCP skills extension, SEP-2640.
|
|
3
|
+
*
|
|
4
|
+
* This is an unratified proposal. See {@link ./spec.ts} for the exact revision
|
|
5
|
+
* this code was written against, and `docs/skills.md` for the negotiation,
|
|
6
|
+
* integrity and fallback model.
|
|
7
|
+
*/
|
|
8
|
+
export { DIRECTORY_MIME_TYPE, HONOURED_CACHE_SCOPES, MAX_CACHE_TTL_MS, MAX_SKILL_LIST_PAGES, MAX_SKILL_RESOURCE_ENTRIES, MAX_SKILL_TOTAL_BYTES, SKILL_DIGEST_PATTERN, SKILLS_EXTENSION_NAME, SKILLS_EXTENSION_REVISION, SKILLS_EXTENSION_STATUS, SKILLS_METHODS, describeNegotiation, skillsExtensionDiagnostic, } from "./spec.js";
|
|
9
|
+
export { declaredTotalBytes, DirectoryReadResultSchema, DirectoryResourceSchema, finalPathSegment, findResourceRef, frontmatterDescription, frontmatterName, isDirectoryResource, isValidDigest, resourceSetFingerprint, SkillEntrySchema, SkillResourceRefSchema, SkillsGetResultSchema, SkillsListResultSchema, skillPathOf, SkillValidationError, validateSkillEntry, } from "./protocol.js";
|
|
10
|
+
export { base64ToBytes, computeDigest, SkillIntegrityError, textToBytes, verifyBytes, verifyFrontmatter, verifyNamePath, verifyResourceRead, } from "./integrity.js";
|
|
11
|
+
export { SkillsExtensionClient, } from "./client.js";
|
|
12
|
+
export { discoverSkillsViaExtension } from "./discover.js";
|
|
13
|
+
export { loadSkillDocument, readSkillResource, SkillFetchBudget, } from "./load.js";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type SkillEntry, type SkillResourceRef } from "./protocol.js";
|
|
2
|
+
export type IntegrityFailureCode = "resource_not_listed" | "size_mismatch" | "digest_mismatch" | "malformed_digest" | "frontmatter_mismatch" | "name_path_mismatch" | "dynamic_budget_exceeded" | "no_text_content";
|
|
3
|
+
/** A refusal to use retrieved content. Distinct from a transport or policy error. */
|
|
4
|
+
export declare class SkillIntegrityError extends Error {
|
|
5
|
+
readonly code: IntegrityFailureCode;
|
|
6
|
+
readonly uri: string;
|
|
7
|
+
constructor(code: IntegrityFailureCode, message: string, uri: string);
|
|
8
|
+
}
|
|
9
|
+
/** SHA-256 over raw bytes, formatted the way SEP-2640 writes digests. */
|
|
10
|
+
export declare function computeDigest(bytes: Uint8Array): string;
|
|
11
|
+
/**
|
|
12
|
+
* The exact bytes a digest covers.
|
|
13
|
+
*
|
|
14
|
+
* MCP text resources arrive as a JS string; the digest is over the file's raw
|
|
15
|
+
* bytes, so the string is encoded back to UTF-8 — the encoding MCP uses on the
|
|
16
|
+
* wire — before hashing. Binary resources arrive base64-encoded and are decoded
|
|
17
|
+
* instead. Getting this wrong would make every digest comparison meaningless,
|
|
18
|
+
* which is why it is one function used by every caller.
|
|
19
|
+
*/
|
|
20
|
+
export declare function textToBytes(text: string): Uint8Array;
|
|
21
|
+
export declare function base64ToBytes(data: string): Uint8Array;
|
|
22
|
+
/**
|
|
23
|
+
* Verify retrieved bytes against a declared resource reference.
|
|
24
|
+
*
|
|
25
|
+
* Size is checked first and treated as a verification failure in its own right,
|
|
26
|
+
* exactly like a digest mismatch — a wrong length already proves the bytes are
|
|
27
|
+
* not the declared file, and saying so names the cheaper, more legible failure.
|
|
28
|
+
*/
|
|
29
|
+
export declare function verifyBytes(ref: SkillResourceRef, bytes: Uint8Array): void;
|
|
30
|
+
/**
|
|
31
|
+
* Authorize and verify one retrieved file against its skill entry.
|
|
32
|
+
*
|
|
33
|
+
* A URI absent from the entry's `resources` is a verification failure, not a
|
|
34
|
+
* permissive default: SEP-2640 requires reads to resolve only to listed URIs,
|
|
35
|
+
* so an unlisted file is refused even though the server volunteered it.
|
|
36
|
+
*
|
|
37
|
+
* For `"dynamic"` sets there is nothing to compare against, so the only
|
|
38
|
+
* enforceable constraint is the 16 MiB ceiling applied to what is actually
|
|
39
|
+
* retrieved. `retrievedBytesSoFar` carries the running total for the skill.
|
|
40
|
+
*/
|
|
41
|
+
export declare function verifyResourceRead(entry: SkillEntry, uri: string, bytes: Uint8Array, retrievedBytesSoFar?: number): void;
|
|
42
|
+
/**
|
|
43
|
+
* Reparse SKILL.md's own frontmatter and compare it field-by-field with the
|
|
44
|
+
* listing's `frontmatter`.
|
|
45
|
+
*
|
|
46
|
+
* The listing and the file are two independent claims about the same thing;
|
|
47
|
+
* this is what stops a server advertising a benign description and a benign
|
|
48
|
+
* tool grant while shipping a file that says something else. Comparison is
|
|
49
|
+
* exact and symmetric — a field present in one and absent from the other is a
|
|
50
|
+
* mismatch, and so is a differing value.
|
|
51
|
+
*/
|
|
52
|
+
export declare function verifyFrontmatter(entry: SkillEntry, skillMarkdown: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Re-check the name/path agreement against the *parsed file*, not just the
|
|
55
|
+
* listing. `validateSkillEntry` already checked the listing's copy; this closes
|
|
56
|
+
* the case where the two disagree about which one the path is supposed to match.
|
|
57
|
+
*/
|
|
58
|
+
export declare function verifyNamePath(entry: SkillEntry): void;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEP-2640 content verification.
|
|
3
|
+
*
|
|
4
|
+
* Everything a server hands back crosses this module before it is used. The
|
|
5
|
+
* proposal is explicit that digests are *not* a security boundary — the same
|
|
6
|
+
* party authors both the listing and the content, so a digest proves only that
|
|
7
|
+
* the two agree. That is still worth enforcing: it is what makes silent
|
|
8
|
+
* substitution, truncation, and listing/content drift detectable, and it is what
|
|
9
|
+
* lets an approval be bound to exact bytes rather than to a name.
|
|
10
|
+
*
|
|
11
|
+
* Three independent checks apply to every retrieved file:
|
|
12
|
+
* 1. byte length equals the declared `size`;
|
|
13
|
+
* 2. SHA-256 over the raw bytes equals the declared `digest`;
|
|
14
|
+
* 3. for SKILL.md only, the frontmatter reparsed from those bytes matches the
|
|
15
|
+
* listing's `frontmatter` field-by-field.
|
|
16
|
+
*
|
|
17
|
+
* A failure in any of them means the content MUST NOT be used.
|
|
18
|
+
*/
|
|
19
|
+
import { parseFrontmatter } from "@sammorrowdrums/mcpi";
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { findResourceRef, frontmatterName, isValidDigest, skillPathOf, finalPathSegment, } from "./protocol.js";
|
|
22
|
+
import { MAX_SKILL_TOTAL_BYTES } from "./spec.js";
|
|
23
|
+
/** A refusal to use retrieved content. Distinct from a transport or policy error. */
|
|
24
|
+
export class SkillIntegrityError extends Error {
|
|
25
|
+
code;
|
|
26
|
+
uri;
|
|
27
|
+
constructor(code, message, uri) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "SkillIntegrityError";
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.uri = uri;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** SHA-256 over raw bytes, formatted the way SEP-2640 writes digests. */
|
|
35
|
+
export function computeDigest(bytes) {
|
|
36
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The exact bytes a digest covers.
|
|
40
|
+
*
|
|
41
|
+
* MCP text resources arrive as a JS string; the digest is over the file's raw
|
|
42
|
+
* bytes, so the string is encoded back to UTF-8 — the encoding MCP uses on the
|
|
43
|
+
* wire — before hashing. Binary resources arrive base64-encoded and are decoded
|
|
44
|
+
* instead. Getting this wrong would make every digest comparison meaningless,
|
|
45
|
+
* which is why it is one function used by every caller.
|
|
46
|
+
*/
|
|
47
|
+
export function textToBytes(text) {
|
|
48
|
+
return new Uint8Array(Buffer.from(text, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
export function base64ToBytes(data) {
|
|
51
|
+
return new Uint8Array(Buffer.from(data, "base64"));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Verify retrieved bytes against a declared resource reference.
|
|
55
|
+
*
|
|
56
|
+
* Size is checked first and treated as a verification failure in its own right,
|
|
57
|
+
* exactly like a digest mismatch — a wrong length already proves the bytes are
|
|
58
|
+
* not the declared file, and saying so names the cheaper, more legible failure.
|
|
59
|
+
*/
|
|
60
|
+
export function verifyBytes(ref, bytes) {
|
|
61
|
+
if (!isValidDigest(ref.digest)) {
|
|
62
|
+
throw new SkillIntegrityError("malformed_digest", `Resource ${ref.uri} declares a non-conforming digest "${ref.digest}"; content was discarded unverified.`, ref.uri);
|
|
63
|
+
}
|
|
64
|
+
if (bytes.byteLength !== ref.size) {
|
|
65
|
+
throw new SkillIntegrityError("size_mismatch", `Resource ${ref.uri} was declared as ${ref.size} bytes but ${bytes.byteLength} bytes were returned. ` +
|
|
66
|
+
`The content does not match the listing and was discarded.`, ref.uri);
|
|
67
|
+
}
|
|
68
|
+
const actual = computeDigest(bytes);
|
|
69
|
+
if (actual !== ref.digest) {
|
|
70
|
+
throw new SkillIntegrityError("digest_mismatch", `Resource ${ref.uri} hashed to ${actual} but the listing declared ${ref.digest}. ` +
|
|
71
|
+
`The content does not match the listing and was discarded.`, ref.uri);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Authorize and verify one retrieved file against its skill entry.
|
|
76
|
+
*
|
|
77
|
+
* A URI absent from the entry's `resources` is a verification failure, not a
|
|
78
|
+
* permissive default: SEP-2640 requires reads to resolve only to listed URIs,
|
|
79
|
+
* so an unlisted file is refused even though the server volunteered it.
|
|
80
|
+
*
|
|
81
|
+
* For `"dynamic"` sets there is nothing to compare against, so the only
|
|
82
|
+
* enforceable constraint is the 16 MiB ceiling applied to what is actually
|
|
83
|
+
* retrieved. `retrievedBytesSoFar` carries the running total for the skill.
|
|
84
|
+
*/
|
|
85
|
+
export function verifyResourceRead(entry, uri, bytes, retrievedBytesSoFar = 0) {
|
|
86
|
+
const ref = findResourceRef(entry, uri);
|
|
87
|
+
if (ref === undefined) {
|
|
88
|
+
throw new SkillIntegrityError("resource_not_listed", `Resource ${uri} is not listed in skill ${entry.uri}'s resources, so there is no digest to verify it ` +
|
|
89
|
+
`against. Reads must resolve only to listed URIs.`, uri);
|
|
90
|
+
}
|
|
91
|
+
if (ref === "dynamic") {
|
|
92
|
+
const total = retrievedBytesSoFar + bytes.byteLength;
|
|
93
|
+
if (total > MAX_SKILL_TOTAL_BYTES) {
|
|
94
|
+
throw new SkillIntegrityError("dynamic_budget_exceeded", `Skill ${entry.uri} declares a dynamic resource set and has now retrieved ${total} bytes, above the ` +
|
|
95
|
+
`SEP-2640 ceiling of ${MAX_SKILL_TOTAL_BYTES} bytes (16 MiB).`, uri);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
verifyBytes(ref, bytes);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reparse SKILL.md's own frontmatter and compare it field-by-field with the
|
|
103
|
+
* listing's `frontmatter`.
|
|
104
|
+
*
|
|
105
|
+
* The listing and the file are two independent claims about the same thing;
|
|
106
|
+
* this is what stops a server advertising a benign description and a benign
|
|
107
|
+
* tool grant while shipping a file that says something else. Comparison is
|
|
108
|
+
* exact and symmetric — a field present in one and absent from the other is a
|
|
109
|
+
* mismatch, and so is a differing value.
|
|
110
|
+
*/
|
|
111
|
+
export function verifyFrontmatter(entry, skillMarkdown) {
|
|
112
|
+
const parsed = parseFrontmatter(skillMarkdown);
|
|
113
|
+
const actual = parsed.frontmatter;
|
|
114
|
+
const declared = entry.frontmatter;
|
|
115
|
+
const keys = new Set([...Object.keys(declared), ...Object.keys(actual)]);
|
|
116
|
+
const differences = [];
|
|
117
|
+
for (const key of [...keys].sort()) {
|
|
118
|
+
const inDeclared = Object.hasOwn(declared, key);
|
|
119
|
+
const inActual = Object.hasOwn(actual, key);
|
|
120
|
+
if (!inActual) {
|
|
121
|
+
differences.push(`"${key}" was advertised in the listing but is absent from SKILL.md`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!inDeclared) {
|
|
125
|
+
differences.push(`"${key}" is present in SKILL.md but was not advertised in the listing`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!deepEqual(declared[key], actual[key])) {
|
|
129
|
+
differences.push(`"${key}" differs: listing has ${preview(declared[key])}, SKILL.md has ${preview(actual[key])}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (differences.length > 0) {
|
|
133
|
+
throw new SkillIntegrityError("frontmatter_mismatch", `Skill ${entry.uri} was not loaded: its SKILL.md frontmatter does not match the listing. ` +
|
|
134
|
+
differences.join("; "), entry.uri);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Re-check the name/path agreement against the *parsed file*, not just the
|
|
139
|
+
* listing. `validateSkillEntry` already checked the listing's copy; this closes
|
|
140
|
+
* the case where the two disagree about which one the path is supposed to match.
|
|
141
|
+
*/
|
|
142
|
+
export function verifyNamePath(entry) {
|
|
143
|
+
const name = frontmatterName(entry);
|
|
144
|
+
const skillPath = skillPathOf(entry.uri);
|
|
145
|
+
if (name === undefined || skillPath === undefined || finalPathSegment(skillPath) !== name) {
|
|
146
|
+
throw new SkillIntegrityError("name_path_mismatch", `Skill ${entry.uri} does not agree with its declared name "${name ?? "(none)"}".`, entry.uri);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function deepEqual(left, right) {
|
|
150
|
+
if (left === right)
|
|
151
|
+
return true;
|
|
152
|
+
if (typeof left !== typeof right)
|
|
153
|
+
return false;
|
|
154
|
+
if (left === null || right === null)
|
|
155
|
+
return false;
|
|
156
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
157
|
+
if (!Array.isArray(left) || !Array.isArray(right))
|
|
158
|
+
return false;
|
|
159
|
+
if (left.length !== right.length)
|
|
160
|
+
return false;
|
|
161
|
+
return left.every((value, index) => deepEqual(value, right[index]));
|
|
162
|
+
}
|
|
163
|
+
if (typeof left === "object") {
|
|
164
|
+
const leftRecord = left;
|
|
165
|
+
const rightRecord = right;
|
|
166
|
+
const leftKeys = Object.keys(leftRecord).sort();
|
|
167
|
+
const rightKeys = Object.keys(rightRecord).sort();
|
|
168
|
+
if (leftKeys.length !== rightKeys.length)
|
|
169
|
+
return false;
|
|
170
|
+
if (!leftKeys.every((key, index) => key === rightKeys[index]))
|
|
171
|
+
return false;
|
|
172
|
+
return leftKeys.every((key) => deepEqual(leftRecord[key], rightRecord[key]));
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function preview(value) {
|
|
177
|
+
let text;
|
|
178
|
+
try {
|
|
179
|
+
text = JSON.stringify(value) ?? String(value);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
text = "(unserializable)";
|
|
183
|
+
}
|
|
184
|
+
return text.length > 120 ? `${text.slice(0, 120)}…` : text;
|
|
185
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { McpPolicy, McpResourceSource } from "../../mcp/policy.js";
|
|
2
|
+
import type { SkillEntry } from "./protocol.js";
|
|
3
|
+
/** A verified read of one resource belonging to a skill. */
|
|
4
|
+
export interface VerifiedResource {
|
|
5
|
+
readonly uri: string;
|
|
6
|
+
/** Raw bytes exactly as digested. */
|
|
7
|
+
readonly bytes: Uint8Array;
|
|
8
|
+
/** UTF-8 decoding of {@link bytes}, for text resources. */
|
|
9
|
+
readonly text: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Tracks how many bytes a skill has actually pulled down.
|
|
13
|
+
*
|
|
14
|
+
* Only `"dynamic"` skills need this: a declared resource set is bounded before
|
|
15
|
+
* the first fetch, but a dynamic one can only be bounded by watching what
|
|
16
|
+
* arrives.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SkillFetchBudget {
|
|
19
|
+
private retrieved;
|
|
20
|
+
get bytesRetrieved(): number;
|
|
21
|
+
add(count: number): void;
|
|
22
|
+
}
|
|
23
|
+
export interface ReadSkillResourceOptions {
|
|
24
|
+
readonly policy: McpPolicy;
|
|
25
|
+
readonly entry: SkillEntry;
|
|
26
|
+
readonly serverName: string;
|
|
27
|
+
readonly uri: string;
|
|
28
|
+
readonly budget?: SkillFetchBudget;
|
|
29
|
+
readonly source?: McpResourceSource;
|
|
30
|
+
readonly signal?: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read one resource belonging to a skill and verify it before returning it.
|
|
34
|
+
*
|
|
35
|
+
* Verification happens on the raw bytes, on every read, with no cached
|
|
36
|
+
* "already checked this" shortcut — a digest that was right last time says
|
|
37
|
+
* nothing about the bytes that just arrived.
|
|
38
|
+
*
|
|
39
|
+
* Callers receive content only if it verifies. There is deliberately no way to
|
|
40
|
+
* obtain the unverified bytes.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readSkillResource(options: ReadSkillResourceOptions): Promise<VerifiedResource>;
|
|
43
|
+
/**
|
|
44
|
+
* Load a skill's SKILL.md and prove it is the document the listing described.
|
|
45
|
+
*
|
|
46
|
+
* Three separate checks have to pass, and each catches something the others
|
|
47
|
+
* cannot: the digest proves the bytes are the advertised bytes, the
|
|
48
|
+
* field-by-field frontmatter comparison proves the listing did not describe the
|
|
49
|
+
* skill as one thing while serving another, and the name/path check proves the
|
|
50
|
+
* skill is not impersonating a different slot in the namespace.
|
|
51
|
+
*
|
|
52
|
+
* Returns the full document text; stripping frontmatter is the caller's job,
|
|
53
|
+
* because verification needs the document exactly as it was digested.
|
|
54
|
+
*/
|
|
55
|
+
export declare function loadSkillDocument(options: {
|
|
56
|
+
readonly policy: McpPolicy;
|
|
57
|
+
readonly entry: SkillEntry;
|
|
58
|
+
readonly serverName: string;
|
|
59
|
+
readonly budget?: SkillFetchBudget;
|
|
60
|
+
readonly signal?: AbortSignal;
|
|
61
|
+
}): Promise<VerifiedResource>;
|