toolfactory 0.1.1

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 (228) hide show
  1. package/README.md +166 -0
  2. package/dist/bindings/index.d.ts +23 -0
  3. package/dist/bindings/index.js +29 -0
  4. package/dist/bindings/python.d.ts +33 -0
  5. package/dist/bindings/python.js +805 -0
  6. package/dist/bindings/python.test.d.ts +1 -0
  7. package/dist/bindings/python.test.js +316 -0
  8. package/dist/bindings/typescript.d.ts +39 -0
  9. package/dist/bindings/typescript.js +758 -0
  10. package/dist/bindings/typescript.test.d.ts +1 -0
  11. package/dist/bindings/typescript.test.js +264 -0
  12. package/dist/commands.d.ts +188 -0
  13. package/dist/commands.js +662 -0
  14. package/dist/commands.test.d.ts +1 -0
  15. package/dist/commands.test.js +215 -0
  16. package/dist/e2e.test.d.ts +1 -0
  17. package/dist/e2e.test.js +124 -0
  18. package/dist/hosts/browser.d.ts +10 -0
  19. package/dist/hosts/browser.js +94 -0
  20. package/dist/hosts/github.d.ts +66 -0
  21. package/dist/hosts/github.js +247 -0
  22. package/dist/hosts/openclaw.d.ts +4 -0
  23. package/dist/hosts/openclaw.js +94 -0
  24. package/dist/hosts/template.d.ts +15 -0
  25. package/dist/hosts/template.js +142 -0
  26. package/dist/hosts/web.d.ts +4 -0
  27. package/dist/hosts/web.js +89 -0
  28. package/dist/identity/name.d.ts +20 -0
  29. package/dist/identity/name.js +33 -0
  30. package/dist/identity/name.test.d.ts +1 -0
  31. package/dist/identity/name.test.js +37 -0
  32. package/dist/identity/read.d.ts +9 -0
  33. package/dist/identity/read.js +106 -0
  34. package/dist/identity/read.test.d.ts +1 -0
  35. package/dist/identity/read.test.js +76 -0
  36. package/dist/index.d.ts +6 -0
  37. package/dist/index.js +6 -0
  38. package/dist/introspect/index.d.ts +15 -0
  39. package/dist/introspect/index.js +63 -0
  40. package/dist/model.d.ts +236 -0
  41. package/dist/model.js +214 -0
  42. package/dist/ops.d.ts +255 -0
  43. package/dist/ops.js +297 -0
  44. package/dist/project/apply.d.ts +35 -0
  45. package/dist/project/apply.js +360 -0
  46. package/dist/project/apply.test.d.ts +1 -0
  47. package/dist/project/apply.test.js +285 -0
  48. package/dist/project/gate.d.ts +129 -0
  49. package/dist/project/gate.js +597 -0
  50. package/dist/project/history.d.ts +62 -0
  51. package/dist/project/history.js +41 -0
  52. package/dist/project/load.d.ts +21 -0
  53. package/dist/project/load.js +82 -0
  54. package/dist/project/lock.d.ts +28 -0
  55. package/dist/project/lock.js +42 -0
  56. package/dist/project/plan.d.ts +5 -0
  57. package/dist/project/plan.js +96 -0
  58. package/dist/report/coverage.d.ts +22 -0
  59. package/dist/report/coverage.js +72 -0
  60. package/dist/report/coverage.test.d.ts +1 -0
  61. package/dist/report/coverage.test.js +93 -0
  62. package/dist/surfaces/agent-plugins.d.ts +10 -0
  63. package/dist/surfaces/agent-plugins.js +36 -0
  64. package/dist/surfaces/agents.d.ts +35 -0
  65. package/dist/surfaces/agents.js +609 -0
  66. package/dist/surfaces/agents.template.d.ts +8 -0
  67. package/dist/surfaces/agents.template.js +21 -0
  68. package/dist/surfaces/agents.test.d.ts +1 -0
  69. package/dist/surfaces/agents.test.js +181 -0
  70. package/dist/surfaces/browser-extension.d.ts +101 -0
  71. package/dist/surfaces/browser-extension.js +1034 -0
  72. package/dist/surfaces/browser-extension.test.d.ts +1 -0
  73. package/dist/surfaces/browser-extension.test.js +145 -0
  74. package/dist/surfaces/claude.d.ts +2 -0
  75. package/dist/surfaces/claude.js +81 -0
  76. package/dist/surfaces/codex.d.ts +13 -0
  77. package/dist/surfaces/codex.js +101 -0
  78. package/dist/surfaces/codex.test.d.ts +1 -0
  79. package/dist/surfaces/codex.test.js +67 -0
  80. package/dist/surfaces/cursor.d.ts +2 -0
  81. package/dist/surfaces/cursor.js +50 -0
  82. package/dist/surfaces/distribution.test.d.ts +1 -0
  83. package/dist/surfaces/distribution.test.js +146 -0
  84. package/dist/surfaces/dsh.d.ts +25 -0
  85. package/dist/surfaces/dsh.js +147 -0
  86. package/dist/surfaces/dsh.test.d.ts +1 -0
  87. package/dist/surfaces/dsh.test.js +100 -0
  88. package/dist/surfaces/external.d.ts +9 -0
  89. package/dist/surfaces/external.js +4 -0
  90. package/dist/surfaces/gemini.d.ts +29 -0
  91. package/dist/surfaces/gemini.js +68 -0
  92. package/dist/surfaces/gemini.test.d.ts +1 -0
  93. package/dist/surfaces/gemini.test.js +74 -0
  94. package/dist/surfaces/hermes-native.d.ts +53 -0
  95. package/dist/surfaces/hermes-native.js +507 -0
  96. package/dist/surfaces/hermes-native.test.d.ts +1 -0
  97. package/dist/surfaces/hermes-native.test.js +156 -0
  98. package/dist/surfaces/kernel.d.ts +5 -0
  99. package/dist/surfaces/kernel.js +63 -0
  100. package/dist/surfaces/mcp-registry.d.ts +19 -0
  101. package/dist/surfaces/mcp-registry.js +160 -0
  102. package/dist/surfaces/mcp-registry.test.d.ts +1 -0
  103. package/dist/surfaces/mcp-registry.test.js +79 -0
  104. package/dist/surfaces/mcpb.d.ts +11 -0
  105. package/dist/surfaces/mcpb.js +118 -0
  106. package/dist/surfaces/mcpb.test.d.ts +1 -0
  107. package/dist/surfaces/mcpb.test.js +104 -0
  108. package/dist/surfaces/npm.d.ts +6 -0
  109. package/dist/surfaces/npm.js +72 -0
  110. package/dist/surfaces/openclaw-native.d.ts +93 -0
  111. package/dist/surfaces/openclaw-native.js +956 -0
  112. package/dist/surfaces/openclaw-native.test.d.ts +1 -0
  113. package/dist/surfaces/openclaw-native.test.js +246 -0
  114. package/dist/surfaces/pypi.d.ts +5 -0
  115. package/dist/surfaces/pypi.js +82 -0
  116. package/dist/surfaces/readme.d.ts +7 -0
  117. package/dist/surfaces/readme.js +181 -0
  118. package/dist/surfaces/registry.d.ts +6 -0
  119. package/dist/surfaces/registry.js +73 -0
  120. package/dist/surfaces/shared.d.ts +49 -0
  121. package/dist/surfaces/shared.js +112 -0
  122. package/dist/surfaces/skill.d.ts +7 -0
  123. package/dist/surfaces/skill.js +118 -0
  124. package/dist/surfaces/skill.test.d.ts +1 -0
  125. package/dist/surfaces/skill.test.js +87 -0
  126. package/dist/surfaces/web.d.ts +97 -0
  127. package/dist/surfaces/web.js +1285 -0
  128. package/dist/surfaces/web.test.d.ts +1 -0
  129. package/dist/surfaces/web.test.js +255 -0
  130. package/dist/surfaces/workflows.d.ts +2 -0
  131. package/dist/surfaces/workflows.js +809 -0
  132. package/dist/surfaces/workflows.test.d.ts +1 -0
  133. package/dist/surfaces/workflows.test.js +563 -0
  134. package/dist/toolfactory/cli.d.ts +2 -0
  135. package/dist/toolfactory/cli.js +65 -0
  136. package/dist/toolfactory/config.d.ts +3 -0
  137. package/dist/toolfactory/config.js +27 -0
  138. package/dist/toolfactory/mcp.d.ts +37 -0
  139. package/dist/toolfactory/mcp.js +269 -0
  140. package/dist/toolfactory/types.d.ts +31 -0
  141. package/dist/toolfactory/types.js +10 -0
  142. package/dist/toolfactory/web.d.ts +4 -0
  143. package/dist/toolfactory/web.js +40 -0
  144. package/dist/validate/agent-plugins.d.ts +5 -0
  145. package/dist/validate/agent-plugins.js +46 -0
  146. package/package.json +68 -0
  147. package/schemas/agent-plugins/mcp.schema.json +120 -0
  148. package/schemas/agent-plugins/plugin.schema.json +65 -0
  149. package/src/bindings/index.ts +47 -0
  150. package/src/bindings/python.test.ts +374 -0
  151. package/src/bindings/python.ts +862 -0
  152. package/src/bindings/typescript.test.ts +329 -0
  153. package/src/bindings/typescript.ts +818 -0
  154. package/src/commands.test.ts +253 -0
  155. package/src/commands.ts +886 -0
  156. package/src/e2e.test.ts +169 -0
  157. package/src/hosts/browser.ts +106 -0
  158. package/src/hosts/github.ts +321 -0
  159. package/src/hosts/openclaw.ts +110 -0
  160. package/src/hosts/template.ts +149 -0
  161. package/src/hosts/web.ts +111 -0
  162. package/src/identity/name.test.ts +43 -0
  163. package/src/identity/name.ts +39 -0
  164. package/src/identity/read.test.ts +92 -0
  165. package/src/identity/read.ts +119 -0
  166. package/src/index.ts +11 -0
  167. package/src/introspect/index.ts +72 -0
  168. package/src/model.ts +386 -0
  169. package/src/ops.ts +333 -0
  170. package/src/project/apply.test.ts +348 -0
  171. package/src/project/apply.ts +417 -0
  172. package/src/project/gate.ts +717 -0
  173. package/src/project/history.ts +46 -0
  174. package/src/project/load.ts +104 -0
  175. package/src/project/lock.ts +54 -0
  176. package/src/project/plan.ts +101 -0
  177. package/src/report/coverage.test.ts +108 -0
  178. package/src/report/coverage.ts +113 -0
  179. package/src/surfaces/agent-plugins.ts +47 -0
  180. package/src/surfaces/agents.template.ts +32 -0
  181. package/src/surfaces/agents.test.ts +206 -0
  182. package/src/surfaces/agents.ts +729 -0
  183. package/src/surfaces/browser-extension.test.ts +175 -0
  184. package/src/surfaces/browser-extension.ts +1095 -0
  185. package/src/surfaces/claude.ts +99 -0
  186. package/src/surfaces/codex.test.ts +71 -0
  187. package/src/surfaces/codex.ts +105 -0
  188. package/src/surfaces/cursor.ts +62 -0
  189. package/src/surfaces/distribution.test.ts +166 -0
  190. package/src/surfaces/dsh.test.ts +110 -0
  191. package/src/surfaces/dsh.ts +159 -0
  192. package/src/surfaces/external.ts +13 -0
  193. package/src/surfaces/gemini.test.ts +80 -0
  194. package/src/surfaces/gemini.ts +84 -0
  195. package/src/surfaces/hermes-native.test.ts +179 -0
  196. package/src/surfaces/hermes-native.ts +584 -0
  197. package/src/surfaces/kernel.ts +68 -0
  198. package/src/surfaces/mcp-registry.test.ts +99 -0
  199. package/src/surfaces/mcp-registry.ts +190 -0
  200. package/src/surfaces/mcpb.test.ts +112 -0
  201. package/src/surfaces/mcpb.ts +130 -0
  202. package/src/surfaces/npm.ts +78 -0
  203. package/src/surfaces/openclaw-native.test.ts +283 -0
  204. package/src/surfaces/openclaw-native.ts +1070 -0
  205. package/src/surfaces/pypi.ts +84 -0
  206. package/src/surfaces/readme.ts +218 -0
  207. package/src/surfaces/registry.ts +79 -0
  208. package/src/surfaces/shared.ts +140 -0
  209. package/src/surfaces/skill.test.ts +103 -0
  210. package/src/surfaces/skill.ts +136 -0
  211. package/src/surfaces/web.test.ts +303 -0
  212. package/src/surfaces/web.ts +1310 -0
  213. package/src/surfaces/workflows.test.ts +677 -0
  214. package/src/surfaces/workflows.ts +936 -0
  215. package/src/toolfactory/cli.ts +74 -0
  216. package/src/toolfactory/config.ts +33 -0
  217. package/src/toolfactory/mcp.ts +311 -0
  218. package/src/toolfactory/types.ts +52 -0
  219. package/src/toolfactory/web.ts +45 -0
  220. package/src/validate/agent-plugins.ts +53 -0
  221. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  222. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  223. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  224. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  225. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  226. package/web/dist/assets/index-CCXhQKK3.css +2 -0
  227. package/web/dist/assets/index-D1PwZ3qS.js +10 -0
  228. package/web/dist/index.html +14 -0
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Operation, Project } from "../model.js";
3
+ import { registryName, surface } from "./mcp-registry.js";
4
+
5
+ const echo: Operation = { name: "echo", inputSchema: { type: "object" }, requires: [] };
6
+
7
+ function project(overrides: Partial<Project> = {}): Project {
8
+ return {
9
+ root: "/repo",
10
+ tool: {
11
+ schemaVersion: 1,
12
+ identity: "plugin.json",
13
+ binding: "typescript",
14
+ surfaces: ["mcp-registry", "mcp"],
15
+ bundle: { runtime: "package" },
16
+ tests: { examples: {} },
17
+ ...overrides.tool,
18
+ },
19
+ identity: {
20
+ name: "hello",
21
+ version: "1.2.0",
22
+ description: "Say hello",
23
+ repository: "https://github.com/GoatInAHat/hello-tool",
24
+ ...overrides.identity,
25
+ },
26
+ identityExtra: {},
27
+ operations: [echo],
28
+ toolfactoryVersion: "0.1.0",
29
+ packageManager: "npm",
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ function files(target: Project): Record<string, string> {
35
+ return Object.fromEntries(
36
+ surface.plan(target).map((file) => [file.path, file.kind === "file" ? file.content : ""]),
37
+ );
38
+ }
39
+
40
+ describe("mcp-registry Dockerfile", () => {
41
+ it("labels the image with server.json's own name, in the final stage, per binding", () => {
42
+ const ts = files(project());
43
+ const server = JSON.parse(ts["server.json"] ?? "{}");
44
+ expect(server.name).toBe("io.github.GoatInAHat/hello");
45
+ const stages = ts.Dockerfile?.split(/\nFROM /) ?? [];
46
+ expect(stages.at(-1)).toContain(`LABEL io.modelcontextprotocol.server.name="${server.name}"`);
47
+
48
+ const py = files(project({ tool: { ...project().tool, binding: "python" } }));
49
+ const pyServer = JSON.parse(py["server.json"] ?? "{}");
50
+ const pyStages = py.Dockerfile?.split(/\nFROM /) ?? [];
51
+ expect(pyStages.at(-1)).toContain(
52
+ `LABEL io.modelcontextprotocol.server.name="${pyServer.name}"`,
53
+ );
54
+ });
55
+
56
+ it("runs the kernel's mcp entrypoint directly, independent of the packaging step", () => {
57
+ const ts = files(project({ packageManager: "pnpm" }));
58
+ expect(ts.Dockerfile).toContain('ENTRYPOINT ["node","dist/toolfactory/mcp.js"]');
59
+ expect(ts.Dockerfile).toContain("RUN corepack enable");
60
+ expect(ts.Dockerfile).toContain("pnpm install --frozen-lockfile");
61
+
62
+ const npmBuilt = files(project({ packageManager: "npm" }));
63
+ expect(npmBuilt.Dockerfile).toContain("npm ci");
64
+ expect(npmBuilt.Dockerfile).not.toContain("corepack enable");
65
+
66
+ const py = files(project({ tool: { ...project().tool, binding: "python" } }));
67
+ expect(py.Dockerfile).toContain('ENTRYPOINT ["python","-m","hello.toolfactory.mcp"]');
68
+ });
69
+ });
70
+
71
+ describe("mcp-registry server.json oci package", () => {
72
+ it("adds an oci entry with a lowercased ghcr.io identifier, unconditionally", () => {
73
+ const server = JSON.parse(files(project())["server.json"] ?? "{}");
74
+ const oci = server.packages.find((pkg: { registryType: string }) => pkg.registryType === "oci");
75
+ expect(oci).toMatchObject({
76
+ registryType: "oci",
77
+ identifier: "ghcr.io/goatinahat/hello:1.2.0",
78
+ transport: { type: "stdio" },
79
+ });
80
+ // No `npm`/`pypi` surface selected, yet the oci entry is still present.
81
+ expect(server.packages).toHaveLength(1);
82
+ });
83
+
84
+ it("adds the npm entry alongside oci once `npm` is also selected", () => {
85
+ const server = JSON.parse(
86
+ files(project({ tool: { ...project().tool, surfaces: ["mcp-registry", "mcp", "npm"] } }))[
87
+ "server.json"
88
+ ] ?? "{}",
89
+ );
90
+ expect(server.packages.map((pkg: { registryType: string }) => pkg.registryType)).toEqual([
91
+ "npm",
92
+ "oci",
93
+ ]);
94
+ });
95
+
96
+ it("still throws registryName's error when the identity carries no GitHub repository", () => {
97
+ expect(() => registryName({ identity: { name: "hello" } })).toThrow(/GitHub repository/);
98
+ });
99
+ });
@@ -0,0 +1,190 @@
1
+ /**
2
+ * MCP Registry: `server.json` plus the package ownership marker, and the root `Dockerfile`
3
+ * that makes the OCI leg of `server.json` real. Publishing is the `mcp-publisher` Go binary
4
+ * and, for the image, `docker/build-push-action`, both in CI; nothing runs here.
5
+ */
6
+ import { pythonPackage } from "../bindings/python.js";
7
+ import { KERNEL_DIR } from "../bindings/typescript.js";
8
+ import { githubOwner, projectName } from "../identity/name.js";
9
+ import type { Project, Surface } from "../model.js";
10
+ import {
11
+ compact,
12
+ configProperties,
13
+ envName,
14
+ has,
15
+ isSensitive,
16
+ json,
17
+ npmName,
18
+ pypiName,
19
+ requiredConfig,
20
+ } from "./shared.js";
21
+
22
+ export const SERVER_SCHEMA_ID =
23
+ "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json";
24
+
25
+ /** `owner` from the identity file's GitHub repository, or the error every registry name needs it for. */
26
+ function requireGithubOwner(identity: { repository?: string }): string {
27
+ const owner = githubOwner(identity.repository);
28
+ if (!owner) {
29
+ throw new Error(
30
+ 'Surface "mcp-registry" needs a GitHub repository URL in the identity file to derive io.github.<owner>/<name>.',
31
+ );
32
+ }
33
+ return owner;
34
+ }
35
+
36
+ export function registryName(project: { identity: { name: string; repository?: string } }): string {
37
+ return projectName.mcpRegistry(project.identity.name, requireGithubOwner(project.identity));
38
+ }
39
+
40
+ /** The registry schema caps description at 100 characters; cut at a word boundary. */
41
+ export function registryDescription(text: string): string {
42
+ if (text.length <= 100) return text;
43
+ const cut = text.slice(0, 100);
44
+ return cut.slice(0, Math.max(cut.lastIndexOf(" "), 1)).replace(/[\s,;:/-]+$/, "");
45
+ }
46
+
47
+ /** `ghcr.io` requires a lowercase path; the canonical name N is already lowercase (`NAME_PATTERN`). */
48
+ export function ociImage(project: Project, owner: string): string {
49
+ const version = project.identity.version ?? "0.0.0";
50
+ return `ghcr.io/${owner.toLowerCase()}/${project.identity.name}:${version}`;
51
+ }
52
+
53
+ /**
54
+ * The kernel's own `mcp` entrypoint (stdio, the only transport the image can serve —
55
+ * `mcp.ts`/`mcp.py` binds no host flag), invoked directly so the image never depends on
56
+ * whether `cli` or `mcp` happen to be selected: `kernel()` writes this file for every project
57
+ * regardless of surface selection (`src/project/plan.ts`).
58
+ */
59
+ function dockerEntrypoint(project: Project): string[] {
60
+ return project.tool.binding === "python"
61
+ ? ["python", "-m", `${pythonPackage(project)}.toolfactory.mcp`]
62
+ : ["node", `${KERNEL_DIR.replace(/^src\//, "dist/")}/mcp.js`];
63
+ }
64
+
65
+ const NODE_IMAGE = "node:24-alpine";
66
+ const PYTHON_IMAGE = "python:3.12-slim-bookworm";
67
+ const UV_IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm-slim";
68
+
69
+ /** What each package manager spells inside the build stage; mirrors `workflows.ts`'s own table. */
70
+ const DOCKER_PACKAGE_MANAGERS: Record<
71
+ "npm" | "pnpm",
72
+ { lockfile: string; corepack: boolean; install: string; build: string; prune: string }
73
+ > = {
74
+ npm: {
75
+ lockfile: "package-lock.json",
76
+ corepack: false,
77
+ install: "npm ci",
78
+ build: "npm run build",
79
+ prune: "npm prune --omit=dev",
80
+ },
81
+ pnpm: {
82
+ lockfile: "pnpm-lock.yaml",
83
+ corepack: true,
84
+ install: "pnpm install --frozen-lockfile",
85
+ build: "pnpm run build",
86
+ prune: "pnpm prune --prod",
87
+ },
88
+ };
89
+
90
+ /**
91
+ * The root `Dockerfile`, stdio only (§4.1: no `--host` flag exists to bind past loopback), one
92
+ * multi-stage build per binding. `label` is `server.json`'s own `name`, byte for byte — the MCP
93
+ * registry's OCI ownership check reads it back out of the image config (`LABEL`, final stage).
94
+ */
95
+ export function dockerfileTemplate(project: Project, label: string): string {
96
+ if (project.tool.binding === "python") {
97
+ return `# syntax=docker/dockerfile:1
98
+ FROM ${UV_IMAGE} AS build
99
+ WORKDIR /app
100
+ COPY pyproject.toml ./
101
+ COPY src ./src
102
+ RUN uv sync --no-dev
103
+
104
+ FROM ${PYTHON_IMAGE}
105
+ LABEL io.modelcontextprotocol.server.name="${label}"
106
+ WORKDIR /app
107
+ COPY --from=build /app/.venv ./.venv
108
+ COPY --from=build /app/src ./src
109
+ COPY --from=build /app/pyproject.toml ./
110
+ ENV PATH="/app/.venv/bin:$PATH"
111
+ ENTRYPOINT ${JSON.stringify(dockerEntrypoint(project))}
112
+ `;
113
+ }
114
+ const pm = DOCKER_PACKAGE_MANAGERS[project.packageManager ?? "npm"];
115
+ return `# syntax=docker/dockerfile:1
116
+ FROM ${NODE_IMAGE} AS build
117
+ WORKDIR /app
118
+ ${pm.corepack ? "RUN corepack enable\n" : ""}COPY package.json ${pm.lockfile} ./
119
+ RUN ${pm.install}
120
+ COPY . .
121
+ RUN ${pm.build} && ${pm.prune}
122
+
123
+ FROM ${NODE_IMAGE}
124
+ LABEL io.modelcontextprotocol.server.name="${label}"
125
+ WORKDIR /app
126
+ COPY --from=build /app/node_modules ./node_modules
127
+ COPY --from=build /app/dist ./dist
128
+ COPY --from=build /app/package.json ./
129
+ ENTRYPOINT ${JSON.stringify(dockerEntrypoint(project))}
130
+ `;
131
+ }
132
+
133
+ export const surface: Surface = {
134
+ id: "mcp-registry",
135
+ plan(project) {
136
+ const { identity } = project;
137
+ const version = identity.version ?? "0.0.0";
138
+ const owner = requireGithubOwner(identity);
139
+ const name = projectName.mcpRegistry(identity.name, owner);
140
+ const environmentVariables = Object.entries(configProperties(project)).map(([key, property]) =>
141
+ compact({
142
+ name: envName(key),
143
+ description: property.description,
144
+ isRequired: requiredConfig(project).includes(key) || undefined,
145
+ isSecret: isSensitive(property) || undefined,
146
+ }),
147
+ );
148
+ const withEnv = <T extends Record<string, unknown>>(entry: T) =>
149
+ compact({
150
+ ...entry,
151
+ environmentVariables: environmentVariables.length ? environmentVariables : undefined,
152
+ });
153
+ const pkg =
154
+ project.tool.binding === "python"
155
+ ? {
156
+ registryType: "pypi",
157
+ identifier: pypiName(project),
158
+ version,
159
+ transport: { type: "stdio" },
160
+ }
161
+ : {
162
+ registryType: "npm",
163
+ identifier: npmName(project),
164
+ version,
165
+ transport: { type: "stdio" },
166
+ };
167
+ // The OCI leg is unconditional: the root Dockerfile it names ships from every mcp-registry
168
+ // selection (the kernel's mcp.ts/mcp.py already exists for every project, D10), unlike the
169
+ // npm/pypi entry, which only exists when that distribution surface is also selected.
170
+ const oci = withEnv({
171
+ registryType: "oci",
172
+ identifier: ociImage(project, owner),
173
+ transport: { type: "stdio" },
174
+ });
175
+ const packages = [...(has(project, "npm") || has(project, "pypi") ? [withEnv(pkg)] : []), oci];
176
+ const server = compact({
177
+ $schema: SERVER_SCHEMA_ID,
178
+ name,
179
+ description: registryDescription(identity.description ?? identity.name),
180
+ version,
181
+ repository: identity.repository ? { url: identity.repository, source: "github" } : undefined,
182
+ websiteUrl: identity.homepage,
183
+ packages,
184
+ });
185
+ return [
186
+ { kind: "file", path: "server.json", content: json(server) },
187
+ { kind: "file", path: "Dockerfile", content: dockerfileTemplate(project, name) },
188
+ ];
189
+ },
190
+ };
@@ -0,0 +1,112 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { NODE_ENGINES } from "../bindings/typescript.js";
3
+ import type { Operation, Project } from "../model.js";
4
+ import { MCPB_PIN, surface as mcpb } from "./mcpb.js";
5
+
6
+ const echo: Operation = {
7
+ name: "echo",
8
+ description: "Echoes.",
9
+ inputSchema: { type: "object" },
10
+ requires: [],
11
+ };
12
+ const browse: Operation = {
13
+ name: "browse",
14
+ inputSchema: { type: "object" },
15
+ requires: ["browser"],
16
+ };
17
+
18
+ const project: Project = {
19
+ root: "/repo",
20
+ tool: {
21
+ schemaVersion: 1,
22
+ identity: "plugin.json",
23
+ binding: "typescript",
24
+ surfaces: ["mcp", "npm", "mcpb"],
25
+ bundle: { runtime: "package" },
26
+ tests: { examples: {} },
27
+ config: {
28
+ properties: {
29
+ apiKey: { type: "string", title: "API key", "x-toolfactory": { sensitive: true } },
30
+ region: { type: "string", description: "Service region." },
31
+ },
32
+ required: ["apiKey"],
33
+ },
34
+ },
35
+ identity: {
36
+ name: "hello.tool",
37
+ version: "0.1.0",
38
+ description: "Says hello.",
39
+ author: { name: "Ada" },
40
+ homepage: "https://example.com",
41
+ repository: "https://github.com/ada/hello",
42
+ license: "MIT",
43
+ },
44
+ identityExtra: {},
45
+ operations: [echo, browse],
46
+ toolfactoryVersion: "0.1.0",
47
+ packageManager: "npm",
48
+ };
49
+
50
+ const [manifestFile, ...rest] = mcpb.plan(project);
51
+ if (manifestFile?.kind !== "file") throw new Error("expected one whole file");
52
+ const manifest = JSON.parse(manifestFile.content) as Record<string, unknown>;
53
+
54
+ describe("mcpb", () => {
55
+ it("is one manifest: the built kernel entry under ${__dirname}, config as user_config, the operations MCP serves", () => {
56
+ expect(rest).toEqual([]);
57
+ expect(manifestFile.path).toBe("hosts/mcpb/manifest.json");
58
+ expect(manifest).toEqual({
59
+ manifest_version: "0.2",
60
+ name: "hello.tool",
61
+ display_name: "hello.tool",
62
+ version: "0.1.0",
63
+ description: "Says hello.",
64
+ author: { name: "Ada" },
65
+ homepage: "https://example.com",
66
+ documentation: "https://example.com",
67
+ repository: { type: "git", url: "https://github.com/ada/hello" },
68
+ license: "MIT",
69
+ server: {
70
+ type: "node",
71
+ // The npm tarball's built entry, the same file the mcp-registry Dockerfile runs.
72
+ entry_point: "dist/toolfactory/mcp.js",
73
+ mcp_config: {
74
+ command: "node",
75
+ args: ["${__dirname}/dist/toolfactory/mcp.js"],
76
+ // Claude Desktop substitutes these from the settings UI `user_config` generates.
77
+ env: { APIKEY: "${user_config.apiKey}", REGION: "${user_config.region}" },
78
+ },
79
+ },
80
+ // `browse` needs a host capability MCP cannot serve, so the install UI must not list it.
81
+ tools: [{ name: "echo", description: "Echoes." }],
82
+ user_config: {
83
+ apiKey: {
84
+ type: "string",
85
+ title: "API key",
86
+ description: "apiKey",
87
+ required: true,
88
+ sensitive: true,
89
+ },
90
+ region: { type: "string", title: "region", description: "Service region." },
91
+ },
92
+ compatibility: { runtimes: { node: NODE_ENGINES } },
93
+ });
94
+ // No privacy_policies unless tool.json names them (see the module docs).
95
+ expect(manifestFile.content).not.toContain("privacy_policies");
96
+ });
97
+
98
+ it("declares the kernel's own Node floor, and refuses a binding whose package is not the bundle", () => {
99
+ expect(() => mcpb.plan({ ...project, tool: { ...project.tool, binding: "python" } })).toThrow(
100
+ /requires the typescript binding/,
101
+ );
102
+
103
+ expect(mcpb.validate?.(project)).toEqual([
104
+ {
105
+ label: "mcpb validate",
106
+ command: "npx",
107
+ args: ["-y", `@anthropic-ai/mcpb@${MCPB_PIN}`, "validate", "hosts/mcpb/manifest.json"],
108
+ cwd: "/repo",
109
+ },
110
+ ]);
111
+ });
112
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * MCPB (MCP Bundle): the one-click `.mcpb` Claude Desktop installs, and the only channel
3
+ * Anthropic's Connectors Directory takes a *local* server through.
4
+ *
5
+ * A bundle is a zip whose root is the server plus its dependencies plus a `manifest.json`. That
6
+ * root is a staging tree, not the repository: the payload is exactly the npm tarball with
7
+ * production dependencies installed into it, which `src/project/gate.ts` builds under
8
+ * `dist/mcpb/` at package time. So this surface emits one tracked file — the manifest — and the
9
+ * packaging step copies it in; nothing here duplicates what `npm pack` already decides ships.
10
+ *
11
+ * `manifest_version` is `0.2`, which is what `mcpb init` itself writes for a new bundle (0.3 and
12
+ * 0.4 exist, and add icon variants, localization and the `uv` server type — none of which this
13
+ * projection uses) and the floor Anthropic's directory submission requires.
14
+ *
15
+ * `privacy_policies` (the URLs of the privacy policies of the services a bundle talks to, which
16
+ * Anthropic's directory submission requires) comes from `tool.json` `mcpb.privacyPolicies`; a
17
+ * homepage is not one, so nothing is invented when the key is absent.
18
+ */
19
+ import { KERNEL_DIR, NODE_ENGINES } from "../bindings/typescript.js";
20
+ import type { Project, Surface } from "../model.js";
21
+ import { compact, configProperties, envName, isSensitive, json, mcpVerdict } from "./shared.js";
22
+
23
+ export const HOST_DIR = "hosts/mcpb";
24
+ export const MANIFEST_PATH = `${HOST_DIR}/manifest.json`;
25
+
26
+ /**
27
+ * The `@anthropic-ai/mcpb` release `validate()` and the packaging step run. Pinned so the gate is
28
+ * reproducible; Renovate bumps this constant (`renovate.json` `customManagers`).
29
+ */
30
+ export const MCPB_PIN = "2.1.2";
31
+
32
+ /** What `mcpb init` writes for a new bundle, and the floor the directory submission requires. */
33
+ export const MANIFEST_VERSION = "0.2";
34
+
35
+ /**
36
+ * The kernel's built `mcp` entry inside the bundle root — the same file the `mcp-registry`
37
+ * surface's Dockerfile runs, because both start from the published package's `dist/`.
38
+ */
39
+ function entryPoint(): string {
40
+ // A function, not a module-level constant: `mcpb` sits in an import cycle with the
41
+ // TypeScript binding, and a `const` derived from another module's `const` at evaluation
42
+ // time is only defined for one of the two possible orders.
43
+ return `${KERNEL_DIR.replace(/^src\//, "dist/")}/mcp.js`;
44
+ }
45
+
46
+ /**
47
+ * `compatibility.runtimes.node`: the kernel's own floor, mirroring the TypeScript scaffold's
48
+ * `engines.node` (`mcpb.test.ts` fails when the two drift apart). Claude Desktop ships a Node it
49
+ * runs bundles with, and reads this to refuse a bundle that one is too old for.
50
+ */
51
+
52
+ /** JSON Schema types `user_config` has a field type for; anything else is collected as text. */
53
+ const USER_CONFIG_TYPES = new Set(["string", "number", "boolean"]);
54
+
55
+ export const surface: Surface = {
56
+ id: "mcpb",
57
+ plan(project: Project) {
58
+ if (project.tool.binding !== "typescript") {
59
+ // A python bundle would have to vendor an interpreter's site-packages into the bundle root
60
+ // (Claude Desktop ships Node, not Python), which `npm pack` plus `npm install` gives the
61
+ // TypeScript binding for free and `uv` gives no equivalent of.
62
+ throw new Error(
63
+ 'Surface "mcpb" requires the typescript binding: a bundle root is the npm tarball with its production dependencies installed into it.',
64
+ );
65
+ }
66
+ const { identity } = project;
67
+ const properties = configProperties(project);
68
+ const required = new Set(project.tool.config?.required as string[] | undefined);
69
+ const userConfig = Object.entries(properties).map(([key, property]) => [
70
+ key,
71
+ compact({
72
+ type: USER_CONFIG_TYPES.has(property.type as string) ? property.type : "string",
73
+ title: (property.title as string | undefined) ?? key,
74
+ description: (property.description as string | undefined) ?? key,
75
+ required: required.has(key) || undefined,
76
+ sensitive: isSensitive(property) || undefined,
77
+ }),
78
+ ]);
79
+ // Claude Desktop substitutes `${user_config.K}` from the settings UI it generates, so the
80
+ // kernel reads its configuration from the same environment names it does everywhere else.
81
+ const env = Object.fromEntries(
82
+ Object.keys(properties).map((key) => [envName(key), `\${user_config.${key}}`]),
83
+ );
84
+ const manifest = compact({
85
+ manifest_version: MANIFEST_VERSION,
86
+ name: identity.name,
87
+ display_name: identity.name,
88
+ version: identity.version,
89
+ description: identity.description,
90
+ author: identity.author?.name ? identity.author : { name: identity.name },
91
+ homepage: identity.homepage,
92
+ documentation: identity.homepage,
93
+ repository: identity.repository ? { type: "git", url: identity.repository } : undefined,
94
+ license: identity.license,
95
+ privacy_policies: project.tool.mcpb?.privacyPolicies,
96
+ keywords: identity.keywords,
97
+ server: {
98
+ type: "node",
99
+ entry_point: entryPoint(),
100
+ // `${__dirname}` is the installed bundle's own directory: an install is an unzip
101
+ // somewhere else, so nothing in the launch may be repository-relative.
102
+ mcp_config: compact({
103
+ command: "node",
104
+ args: [`\${__dirname}/${entryPoint()}`],
105
+ env: Object.keys(env).length ? env : undefined,
106
+ }),
107
+ },
108
+ // Exactly the operations the kernel's own `tools/list` serves (§4.3): the manifest is what
109
+ // the install UI shows before the server ever runs, so it must not promise more.
110
+ tools: project.operations
111
+ .filter((operation) => mcpVerdict(operation).kind !== "excluded")
112
+ .map((operation) => compact({ name: operation.name, description: operation.description })),
113
+ user_config: userConfig.length ? Object.fromEntries(userConfig) : undefined,
114
+ compatibility: { runtimes: { node: NODE_ENGINES } },
115
+ });
116
+ return [{ kind: "file", path: MANIFEST_PATH, content: json(manifest) }];
117
+ },
118
+ validate(project) {
119
+ return [
120
+ {
121
+ label: "mcpb validate",
122
+ command: "npx",
123
+ args: ["-y", `@anthropic-ai/mcpb@${MCPB_PIN}`, "validate", MANIFEST_PATH],
124
+ cwd: project.root,
125
+ },
126
+ ];
127
+ },
128
+ // The bundle carries MCP tool calls and nothing else: the rule is the `mcp` surface's own.
129
+ verdict: mcpVerdict,
130
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * npm package: identity projected into package.json (merge; the author owns the rest),
3
+ * the CLI bin, and the MCP Registry ownership marker.
4
+ */
5
+
6
+ import { githubSlug } from "../hosts/github.js";
7
+ import type { Surface } from "../model.js";
8
+ import { registryName } from "./mcp-registry.js";
9
+ import { compact, has, npmName } from "./shared.js";
10
+ import { WEB_DIR } from "./web.js";
11
+
12
+ /**
13
+ * npm's object form. A bare URL string is only valid as one of npm's shorthands, so `publint`
14
+ * rejects the one an identity file carries; the slug keeps its authored case, because GitHub
15
+ * owner and repository names are case-preserving.
16
+ */
17
+ function repositoryField(
18
+ repository: string | undefined,
19
+ ): { type: string; url: string } | undefined {
20
+ if (!repository) return undefined;
21
+ const slug = githubSlug(repository);
22
+ return { type: "git", url: slug ? `git+https://github.com/${slug}.git` : repository };
23
+ }
24
+
25
+ export const surface: Surface = {
26
+ id: "npm",
27
+ plan(project) {
28
+ if (project.tool.binding !== "typescript") {
29
+ throw new Error('Surface "npm" requires the typescript binding.');
30
+ }
31
+ const { identity } = project;
32
+ const authored = project.tool.identity === "package.json";
33
+ const patch = compact({
34
+ name: authored ? undefined : npmName(project),
35
+ version: authored ? undefined : identity.version,
36
+ description: authored ? undefined : identity.description,
37
+ license: authored ? undefined : identity.license,
38
+ homepage: authored ? undefined : identity.homepage,
39
+ repository: authored ? undefined : repositoryField(identity.repository),
40
+ keywords: authored ? undefined : identity.keywords,
41
+ type: "module",
42
+ bin:
43
+ has(project, "cli") || has(project, "mcp")
44
+ ? { [identity.name]: "./dist/toolfactory/cli.js" }
45
+ : undefined,
46
+ // `web/dist` only with the `web` surface: the kernel serves the built page from inside
47
+ // the installed package, so `npx <tool> mcp --http --open` works without a checkout.
48
+ files: [
49
+ "dist",
50
+ "src",
51
+ "schemas",
52
+ ...(has(project, "web") ? [`${WEB_DIR}/dist`] : []),
53
+ "README.md",
54
+ "LICENSE",
55
+ ],
56
+ mcpName: has(project, "mcp-registry") ? registryName(project) : undefined,
57
+ });
58
+ return [{ kind: "merge", path: "package.json", format: "json", patch, owned: ["bin"] }];
59
+ },
60
+ validate(project) {
61
+ return [
62
+ {
63
+ label: "npm pack",
64
+ command: "npm",
65
+ args: ["pack", "--dry-run", "--ignore-scripts"],
66
+ cwd: project.root,
67
+ },
68
+ {
69
+ // The publishing linter: it packs the tarball and reads it the way npm, the CDNs and the
70
+ // bundlers do. `--strict` because its default exit code is 0 even with warnings.
71
+ label: "publint",
72
+ command: "npx",
73
+ args: ["--yes", "publint", "--strict"],
74
+ cwd: project.root,
75
+ },
76
+ ];
77
+ },
78
+ };