@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,90 @@
|
|
|
1
|
+
import { base64ToBytes, SkillIntegrityError, textToBytes, verifyFrontmatter, verifyNamePath, verifyResourceRead, } from "./integrity.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tracks how many bytes a skill has actually pulled down.
|
|
4
|
+
*
|
|
5
|
+
* Only `"dynamic"` skills need this: a declared resource set is bounded before
|
|
6
|
+
* the first fetch, but a dynamic one can only be bounded by watching what
|
|
7
|
+
* arrives.
|
|
8
|
+
*/
|
|
9
|
+
export class SkillFetchBudget {
|
|
10
|
+
retrieved = 0;
|
|
11
|
+
get bytesRetrieved() {
|
|
12
|
+
return this.retrieved;
|
|
13
|
+
}
|
|
14
|
+
add(count) {
|
|
15
|
+
this.retrieved += count;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Read one resource belonging to a skill and verify it before returning it.
|
|
20
|
+
*
|
|
21
|
+
* Verification happens on the raw bytes, on every read, with no cached
|
|
22
|
+
* "already checked this" shortcut — a digest that was right last time says
|
|
23
|
+
* nothing about the bytes that just arrived.
|
|
24
|
+
*
|
|
25
|
+
* Callers receive content only if it verifies. There is deliberately no way to
|
|
26
|
+
* obtain the unverified bytes.
|
|
27
|
+
*/
|
|
28
|
+
export async function readSkillResource(options) {
|
|
29
|
+
const { policy, entry, serverName, uri, budget, signal } = options;
|
|
30
|
+
const result = await policy.readResource({
|
|
31
|
+
// SEP-2640 reads are authorized by exact membership in this entry's
|
|
32
|
+
// `resources` set, which the policy indexes per skill. Defaulting to the
|
|
33
|
+
// legacy `skill-load` source would consult the broader per-server skill
|
|
34
|
+
// index instead, which does not contain supporting files at all and is
|
|
35
|
+
// wider than the spec allows for the ones it does contain.
|
|
36
|
+
source: options.source ?? "skills-extension",
|
|
37
|
+
serverName,
|
|
38
|
+
uri,
|
|
39
|
+
skillUri: entry.uri,
|
|
40
|
+
...(signal ? { signal } : {}),
|
|
41
|
+
});
|
|
42
|
+
const content = result.contents.find((item) => item.uri === uri) ?? result.contents[0];
|
|
43
|
+
if (!content) {
|
|
44
|
+
throw new SkillIntegrityError("no_text_content", `Read of ${uri} returned no content to verify.`, uri);
|
|
45
|
+
}
|
|
46
|
+
const bytes = contentBytes(content, uri);
|
|
47
|
+
verifyResourceRead(entry, uri, bytes, budget?.bytesRetrieved ?? 0);
|
|
48
|
+
budget?.add(bytes.length);
|
|
49
|
+
return { uri, bytes, text: new TextDecoder().decode(bytes) };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Load a skill's SKILL.md and prove it is the document the listing described.
|
|
53
|
+
*
|
|
54
|
+
* Three separate checks have to pass, and each catches something the others
|
|
55
|
+
* cannot: the digest proves the bytes are the advertised bytes, the
|
|
56
|
+
* field-by-field frontmatter comparison proves the listing did not describe the
|
|
57
|
+
* skill as one thing while serving another, and the name/path check proves the
|
|
58
|
+
* skill is not impersonating a different slot in the namespace.
|
|
59
|
+
*
|
|
60
|
+
* Returns the full document text; stripping frontmatter is the caller's job,
|
|
61
|
+
* because verification needs the document exactly as it was digested.
|
|
62
|
+
*/
|
|
63
|
+
export async function loadSkillDocument(options) {
|
|
64
|
+
const { policy, entry, serverName, budget, signal } = options;
|
|
65
|
+
verifyNamePath(entry);
|
|
66
|
+
const resource = await readSkillResource({
|
|
67
|
+
policy,
|
|
68
|
+
entry,
|
|
69
|
+
serverName,
|
|
70
|
+
uri: entry.uri,
|
|
71
|
+
...(budget ? { budget } : {}),
|
|
72
|
+
...(signal ? { signal } : {}),
|
|
73
|
+
});
|
|
74
|
+
verifyFrontmatter(entry, resource.text);
|
|
75
|
+
return resource;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Recover the raw bytes a resource content block represents.
|
|
79
|
+
*
|
|
80
|
+
* Text blocks are re-encoded as UTF-8 because that is what the digest covered;
|
|
81
|
+
* decoding and re-encoding is lossless for valid UTF-8 and any input that
|
|
82
|
+
* survives that round trip differently would have failed the digest anyway.
|
|
83
|
+
*/
|
|
84
|
+
function contentBytes(content, uri) {
|
|
85
|
+
if (typeof content.text === "string")
|
|
86
|
+
return textToBytes(content.text);
|
|
87
|
+
if (typeof content.blob === "string")
|
|
88
|
+
return base64ToBytes(content.blob);
|
|
89
|
+
throw new SkillIntegrityError("no_text_content", `Read of ${uri} returned neither text nor blob content.`, uri);
|
|
90
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEP-2640 wire schemas and entry-level validation.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is pure: it turns an untrusted JSON-RPC result into a typed
|
|
5
|
+
* value, or refuses. No network, no filesystem, no policy decisions. The checks
|
|
6
|
+
* that SEP-2640 says are computable "from the listing alone, before any fetch"
|
|
7
|
+
* live here so they can run before a single byte is retrieved.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
/**
|
|
11
|
+
* One declared supporting file. `digest` is the SHA-256 of the file's raw
|
|
12
|
+
* bytes; `size` is the count of those same bytes.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SkillResourceRefSchema: z.ZodObject<{
|
|
15
|
+
uri: z.ZodString;
|
|
16
|
+
digest: z.ZodString;
|
|
17
|
+
size: z.ZodNumber;
|
|
18
|
+
}, z.core.$loose>;
|
|
19
|
+
/**
|
|
20
|
+
* A skill as advertised by `skills/list` / `skills/get`.
|
|
21
|
+
*
|
|
22
|
+
* `frontmatter` is the verbatim YAML-as-JSON the author wrote — not a curated
|
|
23
|
+
* subset — so it is typed as an open record and never narrowed on ingest.
|
|
24
|
+
* `resources` is either the complete declared set or the literal `"dynamic"`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const SkillEntrySchema: z.ZodObject<{
|
|
27
|
+
uri: z.ZodString;
|
|
28
|
+
frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
29
|
+
resources: z.ZodUnion<readonly [z.ZodArray<z.ZodObject<{
|
|
30
|
+
uri: z.ZodString;
|
|
31
|
+
digest: z.ZodString;
|
|
32
|
+
size: z.ZodNumber;
|
|
33
|
+
}, z.core.$loose>>, z.ZodLiteral<"dynamic">]>;
|
|
34
|
+
}, z.core.$loose>;
|
|
35
|
+
export declare const SkillsListResultSchema: z.ZodObject<{
|
|
36
|
+
ttlMs: z.ZodOptional<z.ZodNumber>;
|
|
37
|
+
cacheScope: z.ZodOptional<z.ZodString>;
|
|
38
|
+
resultType: z.ZodOptional<z.ZodString>;
|
|
39
|
+
skills: z.ZodArray<z.ZodObject<{
|
|
40
|
+
uri: z.ZodString;
|
|
41
|
+
frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
42
|
+
resources: z.ZodUnion<readonly [z.ZodArray<z.ZodObject<{
|
|
43
|
+
uri: z.ZodString;
|
|
44
|
+
digest: z.ZodString;
|
|
45
|
+
size: z.ZodNumber;
|
|
46
|
+
}, z.core.$loose>>, z.ZodLiteral<"dynamic">]>;
|
|
47
|
+
}, z.core.$loose>>;
|
|
48
|
+
nextCursor: z.ZodOptional<z.ZodString>;
|
|
49
|
+
}, z.core.$loose>;
|
|
50
|
+
export declare const SkillsGetResultSchema: z.ZodObject<{
|
|
51
|
+
ttlMs: z.ZodOptional<z.ZodNumber>;
|
|
52
|
+
cacheScope: z.ZodOptional<z.ZodString>;
|
|
53
|
+
resultType: z.ZodOptional<z.ZodString>;
|
|
54
|
+
skill: z.ZodObject<{
|
|
55
|
+
uri: z.ZodString;
|
|
56
|
+
frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
57
|
+
resources: z.ZodUnion<readonly [z.ZodArray<z.ZodObject<{
|
|
58
|
+
uri: z.ZodString;
|
|
59
|
+
digest: z.ZodString;
|
|
60
|
+
size: z.ZodNumber;
|
|
61
|
+
}, z.core.$loose>>, z.ZodLiteral<"dynamic">]>;
|
|
62
|
+
}, z.core.$loose>;
|
|
63
|
+
}, z.core.$loose>;
|
|
64
|
+
export declare const DirectoryResourceSchema: z.ZodObject<{
|
|
65
|
+
uri: z.ZodString;
|
|
66
|
+
name: z.ZodOptional<z.ZodString>;
|
|
67
|
+
mimeType: z.ZodOptional<z.ZodString>;
|
|
68
|
+
size: z.ZodOptional<z.ZodNumber>;
|
|
69
|
+
}, z.core.$loose>;
|
|
70
|
+
export declare const DirectoryReadResultSchema: z.ZodObject<{
|
|
71
|
+
resources: z.ZodArray<z.ZodObject<{
|
|
72
|
+
uri: z.ZodString;
|
|
73
|
+
name: z.ZodOptional<z.ZodString>;
|
|
74
|
+
mimeType: z.ZodOptional<z.ZodString>;
|
|
75
|
+
size: z.ZodOptional<z.ZodNumber>;
|
|
76
|
+
}, z.core.$loose>>;
|
|
77
|
+
nextCursor: z.ZodOptional<z.ZodString>;
|
|
78
|
+
}, z.core.$loose>;
|
|
79
|
+
export type SkillResourceRef = z.infer<typeof SkillResourceRefSchema>;
|
|
80
|
+
export type SkillEntry = z.infer<typeof SkillEntrySchema>;
|
|
81
|
+
export type SkillsListResult = z.infer<typeof SkillsListResultSchema>;
|
|
82
|
+
export type SkillsGetResult = z.infer<typeof SkillsGetResultSchema>;
|
|
83
|
+
export type DirectoryResource = z.infer<typeof DirectoryResourceSchema>;
|
|
84
|
+
export type DirectoryReadResult = z.infer<typeof DirectoryReadResultSchema>;
|
|
85
|
+
export type SkillValidationCode = "malformed_result" | "malformed_digest" | "resource_limit_exceeded" | "size_limit_exceeded" | "duplicate_resource_uri" | "skill_md_not_listed" | "missing_frontmatter_field" | "name_path_mismatch" | "dynamic_resources";
|
|
86
|
+
/** A refusal to accept server-supplied skill metadata. Never a fetch failure. */
|
|
87
|
+
export declare class SkillValidationError extends Error {
|
|
88
|
+
readonly code: SkillValidationCode;
|
|
89
|
+
readonly skillUri: string | undefined;
|
|
90
|
+
constructor(code: SkillValidationCode, message: string, skillUri?: string);
|
|
91
|
+
}
|
|
92
|
+
/** True only for exactly `sha256:` + 64 lowercase hex characters. */
|
|
93
|
+
export declare function isValidDigest(digest: string): boolean;
|
|
94
|
+
/** True when a `resources/directory/read` entry describes a directory. */
|
|
95
|
+
export declare function isDirectoryResource(resource: DirectoryResource): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* The `<skill-path>` an entry URI addresses, i.e. everything up to but not
|
|
98
|
+
* including the trailing `/SKILL.md`. Returns `undefined` when the URI does not
|
|
99
|
+
* end in `/SKILL.md`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function skillPathOf(uri: string): string | undefined;
|
|
102
|
+
/** The final path segment of a skill path — the segment that must equal `frontmatter.name`. */
|
|
103
|
+
export declare function finalPathSegment(skillPath: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* A skill's declared name, or `undefined` when frontmatter omits it or it is
|
|
106
|
+
* not a string. `name` and `description` are the two fields SEP-2640 requires.
|
|
107
|
+
*/
|
|
108
|
+
export declare function frontmatterName(entry: SkillEntry): string | undefined;
|
|
109
|
+
export declare function frontmatterDescription(entry: SkillEntry): string | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Validate one listing entry using only information the listing itself carries.
|
|
112
|
+
*
|
|
113
|
+
* This runs before any content is fetched, and every failure means the skill is
|
|
114
|
+
* not loadable — never that it should be silently downgraded or retried against
|
|
115
|
+
* a different contract.
|
|
116
|
+
*
|
|
117
|
+
* Checks, in order:
|
|
118
|
+
* 1. every declared digest is well-formed (`sha256:` + 64 lowercase hex);
|
|
119
|
+
* 2. no duplicate resource URIs within the entry (a duplicate would let one
|
|
120
|
+
* declaration shadow another and make "which digest applies" ambiguous);
|
|
121
|
+
* 3. at most {@link MAX_SKILL_RESOURCE_ENTRIES} entries;
|
|
122
|
+
* 4. at most {@link MAX_SKILL_TOTAL_BYTES} declared bytes in total;
|
|
123
|
+
* 5. the entry's own `uri` appears in its `resources` (SKILL.md counts, and it
|
|
124
|
+
* is the file we are about to fetch — it must carry a digest);
|
|
125
|
+
* 6. `name` and `description` are present;
|
|
126
|
+
* 7. the final `<skill-path>` segment equals `frontmatter.name`.
|
|
127
|
+
*
|
|
128
|
+
* `"dynamic"` resource sets skip 2–5: nothing is declared up front, so the
|
|
129
|
+
* ceilings are enforced against what is actually retrieved instead.
|
|
130
|
+
*/
|
|
131
|
+
export declare function validateSkillEntry(entry: SkillEntry): void;
|
|
132
|
+
/**
|
|
133
|
+
* Total declared bytes for an entry, or `undefined` for a `"dynamic"` set where
|
|
134
|
+
* nothing is declared in advance.
|
|
135
|
+
*/
|
|
136
|
+
export declare function declaredTotalBytes(entry: SkillEntry): number | undefined;
|
|
137
|
+
/** Look up an entry's declaration for one URI. `undefined` means "not listed". */
|
|
138
|
+
export declare function findResourceRef(entry: SkillEntry, uri: string): SkillResourceRef | "dynamic" | undefined;
|
|
139
|
+
/**
|
|
140
|
+
* A stable digest over an entry's declared resource set, used to bind an
|
|
141
|
+
* approval to exact content. Any rotation — a changed digest, an added file, a
|
|
142
|
+
* removed file, or a switch to/from `"dynamic"` — produces a different value,
|
|
143
|
+
* which is what makes a prior approval fall away instead of silently carrying
|
|
144
|
+
* over to different bytes.
|
|
145
|
+
*/
|
|
146
|
+
export declare function resourceSetFingerprint(entry: SkillEntry): string;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEP-2640 wire schemas and entry-level validation.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is pure: it turns an untrusted JSON-RPC result into a typed
|
|
5
|
+
* value, or refuses. No network, no filesystem, no policy decisions. The checks
|
|
6
|
+
* that SEP-2640 says are computable "from the listing alone, before any fetch"
|
|
7
|
+
* live here so they can run before a single byte is retrieved.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { DIRECTORY_MIME_TYPE, MAX_SKILL_RESOURCE_ENTRIES, MAX_SKILL_TOTAL_BYTES, SKILL_DIGEST_PATTERN, } from "./spec.js";
|
|
11
|
+
// -----------------------------------------------------------------------------
|
|
12
|
+
// Wire schemas
|
|
13
|
+
// -----------------------------------------------------------------------------
|
|
14
|
+
/**
|
|
15
|
+
* One declared supporting file. `digest` is the SHA-256 of the file's raw
|
|
16
|
+
* bytes; `size` is the count of those same bytes.
|
|
17
|
+
*/
|
|
18
|
+
export const SkillResourceRefSchema = z.looseObject({
|
|
19
|
+
uri: z.string().min(1),
|
|
20
|
+
digest: z.string().min(1),
|
|
21
|
+
size: z.number().int().nonnegative(),
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* A skill as advertised by `skills/list` / `skills/get`.
|
|
25
|
+
*
|
|
26
|
+
* `frontmatter` is the verbatim YAML-as-JSON the author wrote — not a curated
|
|
27
|
+
* subset — so it is typed as an open record and never narrowed on ingest.
|
|
28
|
+
* `resources` is either the complete declared set or the literal `"dynamic"`.
|
|
29
|
+
*/
|
|
30
|
+
export const SkillEntrySchema = z.looseObject({
|
|
31
|
+
uri: z.string().min(1),
|
|
32
|
+
frontmatter: z.record(z.string(), z.unknown()),
|
|
33
|
+
resources: z.union([z.array(SkillResourceRefSchema), z.literal("dynamic")]),
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* SEP-2549 cache attributes. Advisory freshness metadata only — never an
|
|
37
|
+
* integrity property, and never a substitute for re-verifying bytes.
|
|
38
|
+
*/
|
|
39
|
+
const CacheAttributes = {
|
|
40
|
+
ttlMs: z.number().int().nonnegative().optional(),
|
|
41
|
+
cacheScope: z.string().optional(),
|
|
42
|
+
};
|
|
43
|
+
export const SkillsListResultSchema = z.looseObject({
|
|
44
|
+
// `resultType` appears in the proposal's examples but not its normative field
|
|
45
|
+
// table, so it is tolerated and never required.
|
|
46
|
+
resultType: z.string().optional(),
|
|
47
|
+
skills: z.array(SkillEntrySchema),
|
|
48
|
+
nextCursor: z.string().optional(),
|
|
49
|
+
...CacheAttributes,
|
|
50
|
+
});
|
|
51
|
+
export const SkillsGetResultSchema = z.looseObject({
|
|
52
|
+
resultType: z.string().optional(),
|
|
53
|
+
// Note the singular key: `skills/get` returns `skill`, not `skills`.
|
|
54
|
+
skill: SkillEntrySchema,
|
|
55
|
+
...CacheAttributes,
|
|
56
|
+
});
|
|
57
|
+
export const DirectoryResourceSchema = z.looseObject({
|
|
58
|
+
uri: z.string().min(1),
|
|
59
|
+
name: z.string().optional(),
|
|
60
|
+
mimeType: z.string().optional(),
|
|
61
|
+
size: z.number().int().nonnegative().optional(),
|
|
62
|
+
});
|
|
63
|
+
export const DirectoryReadResultSchema = z.looseObject({
|
|
64
|
+
resources: z.array(DirectoryResourceSchema),
|
|
65
|
+
nextCursor: z.string().optional(),
|
|
66
|
+
});
|
|
67
|
+
/** A refusal to accept server-supplied skill metadata. Never a fetch failure. */
|
|
68
|
+
export class SkillValidationError extends Error {
|
|
69
|
+
code;
|
|
70
|
+
skillUri;
|
|
71
|
+
constructor(code, message, skillUri) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "SkillValidationError";
|
|
74
|
+
this.code = code;
|
|
75
|
+
this.skillUri = skillUri;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// -----------------------------------------------------------------------------
|
|
79
|
+
// Entry validation
|
|
80
|
+
// -----------------------------------------------------------------------------
|
|
81
|
+
/** True only for exactly `sha256:` + 64 lowercase hex characters. */
|
|
82
|
+
export function isValidDigest(digest) {
|
|
83
|
+
return SKILL_DIGEST_PATTERN.test(digest);
|
|
84
|
+
}
|
|
85
|
+
/** True when a `resources/directory/read` entry describes a directory. */
|
|
86
|
+
export function isDirectoryResource(resource) {
|
|
87
|
+
return resource.mimeType === DIRECTORY_MIME_TYPE;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The `<skill-path>` an entry URI addresses, i.e. everything up to but not
|
|
91
|
+
* including the trailing `/SKILL.md`. Returns `undefined` when the URI does not
|
|
92
|
+
* end in `/SKILL.md`.
|
|
93
|
+
*/
|
|
94
|
+
export function skillPathOf(uri) {
|
|
95
|
+
const suffix = "/SKILL.md";
|
|
96
|
+
if (!uri.endsWith(suffix))
|
|
97
|
+
return undefined;
|
|
98
|
+
return uri.slice(0, -suffix.length);
|
|
99
|
+
}
|
|
100
|
+
/** The final path segment of a skill path — the segment that must equal `frontmatter.name`. */
|
|
101
|
+
export function finalPathSegment(skillPath) {
|
|
102
|
+
const trimmed = skillPath.replace(/\/+$/, "");
|
|
103
|
+
const slash = trimmed.lastIndexOf("/");
|
|
104
|
+
return slash === -1 ? trimmed : trimmed.slice(slash + 1);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A skill's declared name, or `undefined` when frontmatter omits it or it is
|
|
108
|
+
* not a string. `name` and `description` are the two fields SEP-2640 requires.
|
|
109
|
+
*/
|
|
110
|
+
export function frontmatterName(entry) {
|
|
111
|
+
const value = entry.frontmatter["name"];
|
|
112
|
+
return typeof value === "string" ? value : undefined;
|
|
113
|
+
}
|
|
114
|
+
export function frontmatterDescription(entry) {
|
|
115
|
+
const value = entry.frontmatter["description"];
|
|
116
|
+
return typeof value === "string" ? value : undefined;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Validate one listing entry using only information the listing itself carries.
|
|
120
|
+
*
|
|
121
|
+
* This runs before any content is fetched, and every failure means the skill is
|
|
122
|
+
* not loadable — never that it should be silently downgraded or retried against
|
|
123
|
+
* a different contract.
|
|
124
|
+
*
|
|
125
|
+
* Checks, in order:
|
|
126
|
+
* 1. every declared digest is well-formed (`sha256:` + 64 lowercase hex);
|
|
127
|
+
* 2. no duplicate resource URIs within the entry (a duplicate would let one
|
|
128
|
+
* declaration shadow another and make "which digest applies" ambiguous);
|
|
129
|
+
* 3. at most {@link MAX_SKILL_RESOURCE_ENTRIES} entries;
|
|
130
|
+
* 4. at most {@link MAX_SKILL_TOTAL_BYTES} declared bytes in total;
|
|
131
|
+
* 5. the entry's own `uri` appears in its `resources` (SKILL.md counts, and it
|
|
132
|
+
* is the file we are about to fetch — it must carry a digest);
|
|
133
|
+
* 6. `name` and `description` are present;
|
|
134
|
+
* 7. the final `<skill-path>` segment equals `frontmatter.name`.
|
|
135
|
+
*
|
|
136
|
+
* `"dynamic"` resource sets skip 2–5: nothing is declared up front, so the
|
|
137
|
+
* ceilings are enforced against what is actually retrieved instead.
|
|
138
|
+
*/
|
|
139
|
+
export function validateSkillEntry(entry) {
|
|
140
|
+
const uri = entry.uri;
|
|
141
|
+
if (entry.resources !== "dynamic") {
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
let totalBytes = 0;
|
|
144
|
+
for (const resource of entry.resources) {
|
|
145
|
+
if (!isValidDigest(resource.digest)) {
|
|
146
|
+
throw new SkillValidationError("malformed_digest", `Skill ${uri} declares resource ${resource.uri} with a non-conforming digest ` +
|
|
147
|
+
`"${resource.digest}". SEP-2640 requires exactly "sha256:" followed by 64 lowercase hex characters.`, uri);
|
|
148
|
+
}
|
|
149
|
+
if (seen.has(resource.uri)) {
|
|
150
|
+
throw new SkillValidationError("duplicate_resource_uri", `Skill ${uri} declares resource ${resource.uri} more than once, so which digest applies is ambiguous.`, uri);
|
|
151
|
+
}
|
|
152
|
+
seen.add(resource.uri);
|
|
153
|
+
totalBytes += resource.size;
|
|
154
|
+
}
|
|
155
|
+
if (entry.resources.length > MAX_SKILL_RESOURCE_ENTRIES) {
|
|
156
|
+
throw new SkillValidationError("resource_limit_exceeded", `Skill ${uri} declares ${entry.resources.length} resources, above the SEP-2640 ceiling of ` +
|
|
157
|
+
`${MAX_SKILL_RESOURCE_ENTRIES}. The skill was not loaded and nothing was fetched.`, uri);
|
|
158
|
+
}
|
|
159
|
+
if (totalBytes > MAX_SKILL_TOTAL_BYTES) {
|
|
160
|
+
throw new SkillValidationError("size_limit_exceeded", `Skill ${uri} declares ${totalBytes} bytes across its resources, above the SEP-2640 ceiling of ` +
|
|
161
|
+
`${MAX_SKILL_TOTAL_BYTES} bytes (16 MiB). The skill was not loaded and nothing was fetched.`, uri);
|
|
162
|
+
}
|
|
163
|
+
if (!seen.has(uri)) {
|
|
164
|
+
throw new SkillValidationError("skill_md_not_listed", `Skill ${uri} does not list its own SKILL.md in its resources, so the content this host would ` +
|
|
165
|
+
`fetch carries no digest to verify against.`, uri);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const name = frontmatterName(entry);
|
|
169
|
+
if (!name) {
|
|
170
|
+
throw new SkillValidationError("missing_frontmatter_field", `Skill ${uri} has no string "name" in its frontmatter.`, uri);
|
|
171
|
+
}
|
|
172
|
+
if (!frontmatterDescription(entry)) {
|
|
173
|
+
throw new SkillValidationError("missing_frontmatter_field", `Skill ${uri} has no string "description" in its frontmatter.`, uri);
|
|
174
|
+
}
|
|
175
|
+
const skillPath = skillPathOf(uri);
|
|
176
|
+
if (skillPath === undefined) {
|
|
177
|
+
throw new SkillValidationError("name_path_mismatch", `Skill URI ${uri} does not address a SKILL.md, so its <skill-path> cannot be compared to "${name}".`, uri);
|
|
178
|
+
}
|
|
179
|
+
const segment = finalPathSegment(skillPath);
|
|
180
|
+
if (segment !== name) {
|
|
181
|
+
throw new SkillValidationError("name_path_mismatch", `Skill ${uri} declares name "${name}" but its final path segment is "${segment}". ` +
|
|
182
|
+
`SEP-2640 requires them to match.`, uri);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Total declared bytes for an entry, or `undefined` for a `"dynamic"` set where
|
|
187
|
+
* nothing is declared in advance.
|
|
188
|
+
*/
|
|
189
|
+
export function declaredTotalBytes(entry) {
|
|
190
|
+
if (entry.resources === "dynamic")
|
|
191
|
+
return undefined;
|
|
192
|
+
return entry.resources.reduce((total, resource) => total + resource.size, 0);
|
|
193
|
+
}
|
|
194
|
+
/** Look up an entry's declaration for one URI. `undefined` means "not listed". */
|
|
195
|
+
export function findResourceRef(entry, uri) {
|
|
196
|
+
if (entry.resources === "dynamic")
|
|
197
|
+
return "dynamic";
|
|
198
|
+
return entry.resources.find((resource) => resource.uri === uri);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* A stable digest over an entry's declared resource set, used to bind an
|
|
202
|
+
* approval to exact content. Any rotation — a changed digest, an added file, a
|
|
203
|
+
* removed file, or a switch to/from `"dynamic"` — produces a different value,
|
|
204
|
+
* which is what makes a prior approval fall away instead of silently carrying
|
|
205
|
+
* over to different bytes.
|
|
206
|
+
*/
|
|
207
|
+
export function resourceSetFingerprint(entry) {
|
|
208
|
+
if (entry.resources === "dynamic")
|
|
209
|
+
return "dynamic";
|
|
210
|
+
return [...entry.resources]
|
|
211
|
+
.map((resource) => `${resource.uri}\u0000${resource.digest}\u0000${resource.size}`)
|
|
212
|
+
.sort()
|
|
213
|
+
.join("\u0001");
|
|
214
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEP-2640 "Skills Extension" — pinned Draft constants.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ DRAFT. SEP-2640 is an Extensions-Track proposal that has **not** been
|
|
5
|
+
* accepted into the MCP specification. Everything in this directory is written
|
|
6
|
+
* against one immutable revision of the proposal and is gated off by default so
|
|
7
|
+
* it can never be mistaken for final-spec support. See `docs/skills.md`.
|
|
8
|
+
*
|
|
9
|
+
* Source of truth for this implementation:
|
|
10
|
+
* repo: modelcontextprotocol/modelcontextprotocol
|
|
11
|
+
* path: docs/seps/2640-skills-extension.mdx
|
|
12
|
+
* commit: 753b9f2be43e07fdd070e535d75f190cff14beea
|
|
13
|
+
*
|
|
14
|
+
* If the proposal moves, this file — not the call sites — is what changes, and
|
|
15
|
+
* the revision below is what makes the drift visible.
|
|
16
|
+
*/
|
|
17
|
+
/** The extension identifier negotiated in `initialize` capabilities (SEP-2133). */
|
|
18
|
+
export declare const SKILLS_EXTENSION_NAME = "io.modelcontextprotocol/skills";
|
|
19
|
+
/** Immutable Draft revision this client was written against. */
|
|
20
|
+
export declare const SKILLS_EXTENSION_REVISION = "753b9f2be43e07fdd070e535d75f190cff14beea";
|
|
21
|
+
/** Proposal status at the pinned revision. Never `"final"` while this reads `"draft"`. */
|
|
22
|
+
export declare const SKILLS_EXTENSION_STATUS = "draft";
|
|
23
|
+
/** JSON-RPC methods defined by the extension. */
|
|
24
|
+
export declare const SKILLS_METHODS: {
|
|
25
|
+
readonly list: "skills/list";
|
|
26
|
+
readonly get: "skills/get";
|
|
27
|
+
readonly directoryRead: "resources/directory/read";
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Maximum number of resource entries a single skill may declare, SKILL.md
|
|
31
|
+
* included. Hosts MUST support up to and including this; servers SHOULD NOT
|
|
32
|
+
* exceed it. Checkable from the listing alone, before any fetch.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MAX_SKILL_RESOURCE_ENTRIES = 512;
|
|
35
|
+
/**
|
|
36
|
+
* Maximum total declared byte size across one skill's resource entries
|
|
37
|
+
* (16 MiB). Also checkable before any fetch. For `"dynamic"` resource sets the
|
|
38
|
+
* same ceiling applies to what is actually retrieved.
|
|
39
|
+
*/
|
|
40
|
+
export declare const MAX_SKILL_TOTAL_BYTES: number;
|
|
41
|
+
/**
|
|
42
|
+
* Digest wire format: exactly `sha256:` followed by 64 **lowercase** hex
|
|
43
|
+
* characters. Uppercase hex, a different prefix, or a different length is
|
|
44
|
+
* non-conforming and this client rejects it rather than normalising it.
|
|
45
|
+
*/
|
|
46
|
+
export declare const SKILL_DIGEST_PATTERN: RegExp;
|
|
47
|
+
/** MIME type a directory entry carries in `resources/directory/read` results. */
|
|
48
|
+
export declare const DIRECTORY_MIME_TYPE = "inode/directory";
|
|
49
|
+
/**
|
|
50
|
+
* `cacheScope` values this client is willing to honour.
|
|
51
|
+
*
|
|
52
|
+
* SEP-2640 delegates `ttlMs`/`cacheScope` semantics to SEP-2549 and does not
|
|
53
|
+
* enumerate the legal scopes itself. Rather than guess the full vocabulary we
|
|
54
|
+
* honour only the two scopes that are unambiguously narrower than a shared
|
|
55
|
+
* cache, and treat every other value — including values we simply have not
|
|
56
|
+
* seen — as "do not cache". Caching is a freshness optimisation; being wrong
|
|
57
|
+
* in the conservative direction only costs a round trip.
|
|
58
|
+
*/
|
|
59
|
+
export declare const HONOURED_CACHE_SCOPES: ReadonlySet<string>;
|
|
60
|
+
/** Upper bound applied to any server-supplied `ttlMs`, so a server cannot pin stale data. */
|
|
61
|
+
export declare const MAX_CACHE_TTL_MS: number;
|
|
62
|
+
/** Page cap for `skills/list` / `resources/directory/read` cursor pagination. */
|
|
63
|
+
export declare const MAX_SKILL_LIST_PAGES = 64;
|
|
64
|
+
/**
|
|
65
|
+
* A single human-readable line naming the proposal, its status, and the exact
|
|
66
|
+
* revision. Surfaced at discovery time so an operator can always tell that this
|
|
67
|
+
* is draft behaviour and which revision produced it.
|
|
68
|
+
*/
|
|
69
|
+
export declare function skillsExtensionDiagnostic(): string;
|
|
70
|
+
/**
|
|
71
|
+
* One-line summary of what a server declared, for logs.
|
|
72
|
+
*
|
|
73
|
+
* Always names the draft status and revision so an operator reading a log can
|
|
74
|
+
* see they are looking at unratified behaviour rather than settled protocol.
|
|
75
|
+
*/
|
|
76
|
+
export declare function describeNegotiation(serverName: string, capability: Record<string, unknown> | undefined): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEP-2640 "Skills Extension" — pinned Draft constants.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ DRAFT. SEP-2640 is an Extensions-Track proposal that has **not** been
|
|
5
|
+
* accepted into the MCP specification. Everything in this directory is written
|
|
6
|
+
* against one immutable revision of the proposal and is gated off by default so
|
|
7
|
+
* it can never be mistaken for final-spec support. See `docs/skills.md`.
|
|
8
|
+
*
|
|
9
|
+
* Source of truth for this implementation:
|
|
10
|
+
* repo: modelcontextprotocol/modelcontextprotocol
|
|
11
|
+
* path: docs/seps/2640-skills-extension.mdx
|
|
12
|
+
* commit: 753b9f2be43e07fdd070e535d75f190cff14beea
|
|
13
|
+
*
|
|
14
|
+
* If the proposal moves, this file — not the call sites — is what changes, and
|
|
15
|
+
* the revision below is what makes the drift visible.
|
|
16
|
+
*/
|
|
17
|
+
/** The extension identifier negotiated in `initialize` capabilities (SEP-2133). */
|
|
18
|
+
export const SKILLS_EXTENSION_NAME = "io.modelcontextprotocol/skills";
|
|
19
|
+
/** Immutable Draft revision this client was written against. */
|
|
20
|
+
export const SKILLS_EXTENSION_REVISION = "753b9f2be43e07fdd070e535d75f190cff14beea";
|
|
21
|
+
/** Proposal status at the pinned revision. Never `"final"` while this reads `"draft"`. */
|
|
22
|
+
export const SKILLS_EXTENSION_STATUS = "draft";
|
|
23
|
+
/** JSON-RPC methods defined by the extension. */
|
|
24
|
+
export const SKILLS_METHODS = {
|
|
25
|
+
list: "skills/list",
|
|
26
|
+
get: "skills/get",
|
|
27
|
+
directoryRead: "resources/directory/read",
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Maximum number of resource entries a single skill may declare, SKILL.md
|
|
31
|
+
* included. Hosts MUST support up to and including this; servers SHOULD NOT
|
|
32
|
+
* exceed it. Checkable from the listing alone, before any fetch.
|
|
33
|
+
*/
|
|
34
|
+
export const MAX_SKILL_RESOURCE_ENTRIES = 512;
|
|
35
|
+
/**
|
|
36
|
+
* Maximum total declared byte size across one skill's resource entries
|
|
37
|
+
* (16 MiB). Also checkable before any fetch. For `"dynamic"` resource sets the
|
|
38
|
+
* same ceiling applies to what is actually retrieved.
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_SKILL_TOTAL_BYTES = 16 * 1024 * 1024;
|
|
41
|
+
/**
|
|
42
|
+
* Digest wire format: exactly `sha256:` followed by 64 **lowercase** hex
|
|
43
|
+
* characters. Uppercase hex, a different prefix, or a different length is
|
|
44
|
+
* non-conforming and this client rejects it rather than normalising it.
|
|
45
|
+
*/
|
|
46
|
+
export const SKILL_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
47
|
+
/** MIME type a directory entry carries in `resources/directory/read` results. */
|
|
48
|
+
export const DIRECTORY_MIME_TYPE = "inode/directory";
|
|
49
|
+
/**
|
|
50
|
+
* `cacheScope` values this client is willing to honour.
|
|
51
|
+
*
|
|
52
|
+
* SEP-2640 delegates `ttlMs`/`cacheScope` semantics to SEP-2549 and does not
|
|
53
|
+
* enumerate the legal scopes itself. Rather than guess the full vocabulary we
|
|
54
|
+
* honour only the two scopes that are unambiguously narrower than a shared
|
|
55
|
+
* cache, and treat every other value — including values we simply have not
|
|
56
|
+
* seen — as "do not cache". Caching is a freshness optimisation; being wrong
|
|
57
|
+
* in the conservative direction only costs a round trip.
|
|
58
|
+
*/
|
|
59
|
+
export const HONOURED_CACHE_SCOPES = new Set(["session", "connection"]);
|
|
60
|
+
/** Upper bound applied to any server-supplied `ttlMs`, so a server cannot pin stale data. */
|
|
61
|
+
export const MAX_CACHE_TTL_MS = 5 * 60_000;
|
|
62
|
+
/** Page cap for `skills/list` / `resources/directory/read` cursor pagination. */
|
|
63
|
+
export const MAX_SKILL_LIST_PAGES = 64;
|
|
64
|
+
/**
|
|
65
|
+
* A single human-readable line naming the proposal, its status, and the exact
|
|
66
|
+
* revision. Surfaced at discovery time so an operator can always tell that this
|
|
67
|
+
* is draft behaviour and which revision produced it.
|
|
68
|
+
*/
|
|
69
|
+
export function skillsExtensionDiagnostic() {
|
|
70
|
+
return (`[skills] SEP-2640 Skills Extension support is DRAFT ` +
|
|
71
|
+
`(${SKILLS_EXTENSION_NAME}, status=${SKILLS_EXTENSION_STATUS}, ` +
|
|
72
|
+
`revision=${SKILLS_EXTENSION_REVISION.slice(0, 12)}). ` +
|
|
73
|
+
`Not final MCP specification; behaviour may change without notice.`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One-line summary of what a server declared, for logs.
|
|
77
|
+
*
|
|
78
|
+
* Always names the draft status and revision so an operator reading a log can
|
|
79
|
+
* see they are looking at unratified behaviour rather than settled protocol.
|
|
80
|
+
*/
|
|
81
|
+
export function describeNegotiation(serverName, capability) {
|
|
82
|
+
const marker = `${SKILLS_EXTENSION_STATUS} ${SKILLS_EXTENSION_REVISION.slice(0, 12)}`;
|
|
83
|
+
if (!capability) {
|
|
84
|
+
return `skills extension (${marker}): "${serverName}" does not declare "${SKILLS_EXTENSION_NAME}" — using legacy skill:// discovery`;
|
|
85
|
+
}
|
|
86
|
+
const directoryRead = capability["directoryRead"] === true;
|
|
87
|
+
return `skills extension (${marker}): "${serverName}" declares "${SKILLS_EXTENSION_NAME}" (directoryRead=${String(directoryRead)})`;
|
|
88
|
+
}
|