@sammorrowdrums/mcpi-ext 0.2.0 → 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.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -47
  3. package/dist/code-mode/eligibility.d.ts +35 -0
  4. package/dist/code-mode/eligibility.js +64 -0
  5. package/dist/code-mode/executor.d.ts +53 -0
  6. package/dist/code-mode/executor.js +185 -0
  7. package/dist/code-mode/index.d.ts +107 -0
  8. package/dist/code-mode/index.js +267 -0
  9. package/dist/code-mode/isolated-vm.d.ts +65 -0
  10. package/dist/code-mode/isolated-vm.js +116 -0
  11. package/dist/code-mode/tools.d.ts +48 -0
  12. package/dist/code-mode/tools.js +73 -0
  13. package/dist/code-mode/type-hints.d.ts +20 -0
  14. package/dist/code-mode/type-hints.js +205 -0
  15. package/dist/docker-e2e.d.ts +54 -0
  16. package/dist/docker-e2e.js +234 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.js +227 -0
  19. package/dist/mcp/call-tool-result.d.ts +10 -0
  20. package/dist/mcp/call-tool-result.js +70 -0
  21. package/dist/mcp/client-factory.d.ts +59 -0
  22. package/dist/mcp/client-factory.js +76 -0
  23. package/dist/mcp/client-manager.d.ts +88 -0
  24. package/dist/mcp/client-manager.js +307 -0
  25. package/dist/mcp/config-loader.d.ts +7 -0
  26. package/dist/mcp/config-loader.js +42 -0
  27. package/dist/mcp/config.d.ts +56 -0
  28. package/dist/mcp/config.js +42 -0
  29. package/dist/mcp/gateway-defaults.d.ts +14 -0
  30. package/dist/mcp/gateway-defaults.js +21 -0
  31. package/dist/mcp/host-approval.d.ts +16 -0
  32. package/dist/mcp/host-approval.js +19 -0
  33. package/dist/mcp/host-elicitation.d.ts +19 -0
  34. package/dist/mcp/host-elicitation.js +94 -0
  35. package/dist/mcp/index.d.ts +8 -0
  36. package/dist/mcp/index.js +7 -0
  37. package/dist/mcp/policy.d.ts +314 -0
  38. package/dist/mcp/policy.js +848 -0
  39. package/dist/routing/facilities.d.ts +109 -0
  40. package/dist/routing/facilities.js +175 -0
  41. package/dist/routing/format.d.ts +24 -0
  42. package/dist/routing/format.js +64 -0
  43. package/dist/routing/index.d.ts +4 -0
  44. package/dist/routing/index.js +4 -0
  45. package/dist/routing/seam.d.ts +35 -0
  46. package/dist/routing/seam.js +28 -0
  47. package/dist/routing/tripwire.d.ts +42 -0
  48. package/dist/routing/tripwire.js +80 -0
  49. package/dist/skills/discover.d.ts +12 -0
  50. package/dist/skills/discover.js +85 -0
  51. package/dist/skills/format.d.ts +13 -0
  52. package/dist/skills/format.js +42 -0
  53. package/dist/skills/index.d.ts +6 -0
  54. package/dist/skills/index.js +6 -0
  55. package/dist/skills/load-skill-tool.d.ts +56 -0
  56. package/dist/skills/load-skill-tool.js +183 -0
  57. package/dist/skills/mcp-tool-proxy.d.ts +7 -0
  58. package/dist/skills/mcp-tool-proxy.js +41 -0
  59. package/dist/skills/sep2640/client.d.ts +91 -0
  60. package/dist/skills/sep2640/client.js +160 -0
  61. package/dist/skills/sep2640/discover.d.ts +33 -0
  62. package/dist/skills/sep2640/discover.js +82 -0
  63. package/dist/skills/sep2640/index.d.ts +13 -0
  64. package/dist/skills/sep2640/index.js +13 -0
  65. package/dist/skills/sep2640/integrity.d.ts +58 -0
  66. package/dist/skills/sep2640/integrity.js +185 -0
  67. package/dist/skills/sep2640/load.d.ts +61 -0
  68. package/dist/skills/sep2640/load.js +90 -0
  69. package/dist/skills/sep2640/protocol.d.ts +146 -0
  70. package/dist/skills/sep2640/protocol.js +214 -0
  71. package/dist/skills/sep2640/spec.d.ts +76 -0
  72. package/dist/skills/sep2640/spec.js +88 -0
  73. package/dist/skills/skill-registry.d.ts +85 -0
  74. package/dist/skills/skill-registry.js +85 -0
  75. package/dist/tool-cli/bridge.d.ts +14 -0
  76. package/dist/tool-cli/bridge.js +196 -0
  77. package/dist/tool-cli/format.d.ts +20 -0
  78. package/dist/tool-cli/format.js +73 -0
  79. package/dist/tool-cli/index.d.ts +7 -0
  80. package/dist/tool-cli/index.js +5 -0
  81. package/dist/tool-cli/provider.d.ts +21 -0
  82. package/dist/tool-cli/provider.js +114 -0
  83. package/dist/tool-cli/startup.d.ts +22 -0
  84. package/dist/tool-cli/startup.js +47 -0
  85. package/package.json +78 -26
  86. package/.dockerignore +0 -6
  87. package/.github/dependabot.yml +0 -25
  88. package/.github/workflows/ci.yml +0 -50
  89. package/.hadolint.yaml +0 -6
  90. package/.prettierignore +0 -4
  91. package/.prettierrc +0 -7
  92. package/AGENTS.md +0 -118
  93. package/DECISIONS.md +0 -99
  94. package/Dockerfile +0 -39
  95. package/aube-lock.yaml +0 -4146
  96. package/docs/code-mode.md +0 -38
  97. package/docs/skills.md +0 -57
  98. package/docs/tool-cli.md +0 -54
  99. package/eslint.config.js +0 -27
  100. package/images/banner.webp +0 -0
  101. package/images/code-c-maude.webp +0 -0
  102. package/images/code-mode-histogram.png +0 -0
  103. package/images/nuclear-mcp-football.webp +0 -0
  104. package/images/skills-enabling-mcp-tools.png +0 -0
  105. package/images/the-skill-dealer.webp +0 -0
  106. package/images/tool-cli-grep.png +0 -0
  107. package/mise.toml +0 -53
  108. package/scripts/docker-entrypoint.sh +0 -29
  109. package/scripts/test-echo-server.mjs +0 -28
  110. package/scripts/test-mcp-integration.mjs +0 -59
  111. package/scripts/test-tool-cli-smoke.mjs +0 -93
  112. package/src/code-mode/code-mode-integration.test.ts +0 -135
  113. package/src/code-mode/eligibility.test.ts +0 -60
  114. package/src/code-mode/eligibility.ts +0 -16
  115. package/src/code-mode/executor.test.ts +0 -168
  116. package/src/code-mode/executor.ts +0 -163
  117. package/src/code-mode/index.ts +0 -208
  118. package/src/code-mode/tools.ts +0 -110
  119. package/src/code-mode/type-hints.test.ts +0 -187
  120. package/src/code-mode/type-hints.ts +0 -249
  121. package/src/docker-e2e.test.ts +0 -195
  122. package/src/docker-e2e.ts +0 -277
  123. package/src/index.test.ts +0 -8
  124. package/src/index.ts +0 -179
  125. package/src/mcp/client-manager.test.ts +0 -250
  126. package/src/mcp/client-manager.ts +0 -199
  127. package/src/mcp/config-loader.test.ts +0 -115
  128. package/src/mcp/config-loader.ts +0 -46
  129. package/src/mcp/config.ts +0 -40
  130. package/src/mcp/index.ts +0 -4
  131. package/src/skills/discover.ts +0 -81
  132. package/src/skills/format.test.ts +0 -70
  133. package/src/skills/format.ts +0 -42
  134. package/src/skills/index.ts +0 -9
  135. package/src/skills/load-skill-tool.ts +0 -136
  136. package/src/skills/mcp-tool-proxy.ts +0 -143
  137. package/src/skills/skill-integration.test.ts +0 -139
  138. package/src/skills/skill-registry.test.ts +0 -83
  139. package/src/skills/skill-registry.ts +0 -69
  140. package/src/test-servers/weather-server.ts +0 -171
  141. package/src/test-servers/weather-stdio.ts +0 -11
  142. package/src/tool-cli/format.test.ts +0 -33
  143. package/src/tool-cli/format.ts +0 -56
  144. package/src/tool-cli/index.ts +0 -3
  145. package/tsconfig.json +0 -21
@@ -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
+ }