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.
- package/README.md +166 -0
- package/dist/bindings/index.d.ts +23 -0
- package/dist/bindings/index.js +29 -0
- package/dist/bindings/python.d.ts +33 -0
- package/dist/bindings/python.js +805 -0
- package/dist/bindings/python.test.d.ts +1 -0
- package/dist/bindings/python.test.js +316 -0
- package/dist/bindings/typescript.d.ts +39 -0
- package/dist/bindings/typescript.js +758 -0
- package/dist/bindings/typescript.test.d.ts +1 -0
- package/dist/bindings/typescript.test.js +264 -0
- package/dist/commands.d.ts +188 -0
- package/dist/commands.js +662 -0
- package/dist/commands.test.d.ts +1 -0
- package/dist/commands.test.js +215 -0
- package/dist/e2e.test.d.ts +1 -0
- package/dist/e2e.test.js +124 -0
- package/dist/hosts/browser.d.ts +10 -0
- package/dist/hosts/browser.js +94 -0
- package/dist/hosts/github.d.ts +66 -0
- package/dist/hosts/github.js +247 -0
- package/dist/hosts/openclaw.d.ts +4 -0
- package/dist/hosts/openclaw.js +94 -0
- package/dist/hosts/template.d.ts +15 -0
- package/dist/hosts/template.js +142 -0
- package/dist/hosts/web.d.ts +4 -0
- package/dist/hosts/web.js +89 -0
- package/dist/identity/name.d.ts +20 -0
- package/dist/identity/name.js +33 -0
- package/dist/identity/name.test.d.ts +1 -0
- package/dist/identity/name.test.js +37 -0
- package/dist/identity/read.d.ts +9 -0
- package/dist/identity/read.js +106 -0
- package/dist/identity/read.test.d.ts +1 -0
- package/dist/identity/read.test.js +76 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/introspect/index.d.ts +15 -0
- package/dist/introspect/index.js +63 -0
- package/dist/model.d.ts +236 -0
- package/dist/model.js +214 -0
- package/dist/ops.d.ts +255 -0
- package/dist/ops.js +297 -0
- package/dist/project/apply.d.ts +35 -0
- package/dist/project/apply.js +360 -0
- package/dist/project/apply.test.d.ts +1 -0
- package/dist/project/apply.test.js +285 -0
- package/dist/project/gate.d.ts +129 -0
- package/dist/project/gate.js +597 -0
- package/dist/project/history.d.ts +62 -0
- package/dist/project/history.js +41 -0
- package/dist/project/load.d.ts +21 -0
- package/dist/project/load.js +82 -0
- package/dist/project/lock.d.ts +28 -0
- package/dist/project/lock.js +42 -0
- package/dist/project/plan.d.ts +5 -0
- package/dist/project/plan.js +96 -0
- package/dist/report/coverage.d.ts +22 -0
- package/dist/report/coverage.js +72 -0
- package/dist/report/coverage.test.d.ts +1 -0
- package/dist/report/coverage.test.js +93 -0
- package/dist/surfaces/agent-plugins.d.ts +10 -0
- package/dist/surfaces/agent-plugins.js +36 -0
- package/dist/surfaces/agents.d.ts +35 -0
- package/dist/surfaces/agents.js +609 -0
- package/dist/surfaces/agents.template.d.ts +8 -0
- package/dist/surfaces/agents.template.js +21 -0
- package/dist/surfaces/agents.test.d.ts +1 -0
- package/dist/surfaces/agents.test.js +181 -0
- package/dist/surfaces/browser-extension.d.ts +101 -0
- package/dist/surfaces/browser-extension.js +1034 -0
- package/dist/surfaces/browser-extension.test.d.ts +1 -0
- package/dist/surfaces/browser-extension.test.js +145 -0
- package/dist/surfaces/claude.d.ts +2 -0
- package/dist/surfaces/claude.js +81 -0
- package/dist/surfaces/codex.d.ts +13 -0
- package/dist/surfaces/codex.js +101 -0
- package/dist/surfaces/codex.test.d.ts +1 -0
- package/dist/surfaces/codex.test.js +67 -0
- package/dist/surfaces/cursor.d.ts +2 -0
- package/dist/surfaces/cursor.js +50 -0
- package/dist/surfaces/distribution.test.d.ts +1 -0
- package/dist/surfaces/distribution.test.js +146 -0
- package/dist/surfaces/dsh.d.ts +25 -0
- package/dist/surfaces/dsh.js +147 -0
- package/dist/surfaces/dsh.test.d.ts +1 -0
- package/dist/surfaces/dsh.test.js +100 -0
- package/dist/surfaces/external.d.ts +9 -0
- package/dist/surfaces/external.js +4 -0
- package/dist/surfaces/gemini.d.ts +29 -0
- package/dist/surfaces/gemini.js +68 -0
- package/dist/surfaces/gemini.test.d.ts +1 -0
- package/dist/surfaces/gemini.test.js +74 -0
- package/dist/surfaces/hermes-native.d.ts +53 -0
- package/dist/surfaces/hermes-native.js +507 -0
- package/dist/surfaces/hermes-native.test.d.ts +1 -0
- package/dist/surfaces/hermes-native.test.js +156 -0
- package/dist/surfaces/kernel.d.ts +5 -0
- package/dist/surfaces/kernel.js +63 -0
- package/dist/surfaces/mcp-registry.d.ts +19 -0
- package/dist/surfaces/mcp-registry.js +160 -0
- package/dist/surfaces/mcp-registry.test.d.ts +1 -0
- package/dist/surfaces/mcp-registry.test.js +79 -0
- package/dist/surfaces/mcpb.d.ts +11 -0
- package/dist/surfaces/mcpb.js +118 -0
- package/dist/surfaces/mcpb.test.d.ts +1 -0
- package/dist/surfaces/mcpb.test.js +104 -0
- package/dist/surfaces/npm.d.ts +6 -0
- package/dist/surfaces/npm.js +72 -0
- package/dist/surfaces/openclaw-native.d.ts +93 -0
- package/dist/surfaces/openclaw-native.js +956 -0
- package/dist/surfaces/openclaw-native.test.d.ts +1 -0
- package/dist/surfaces/openclaw-native.test.js +246 -0
- package/dist/surfaces/pypi.d.ts +5 -0
- package/dist/surfaces/pypi.js +82 -0
- package/dist/surfaces/readme.d.ts +7 -0
- package/dist/surfaces/readme.js +181 -0
- package/dist/surfaces/registry.d.ts +6 -0
- package/dist/surfaces/registry.js +73 -0
- package/dist/surfaces/shared.d.ts +49 -0
- package/dist/surfaces/shared.js +112 -0
- package/dist/surfaces/skill.d.ts +7 -0
- package/dist/surfaces/skill.js +118 -0
- package/dist/surfaces/skill.test.d.ts +1 -0
- package/dist/surfaces/skill.test.js +87 -0
- package/dist/surfaces/web.d.ts +97 -0
- package/dist/surfaces/web.js +1285 -0
- package/dist/surfaces/web.test.d.ts +1 -0
- package/dist/surfaces/web.test.js +255 -0
- package/dist/surfaces/workflows.d.ts +2 -0
- package/dist/surfaces/workflows.js +809 -0
- package/dist/surfaces/workflows.test.d.ts +1 -0
- package/dist/surfaces/workflows.test.js +563 -0
- package/dist/toolfactory/cli.d.ts +2 -0
- package/dist/toolfactory/cli.js +65 -0
- package/dist/toolfactory/config.d.ts +3 -0
- package/dist/toolfactory/config.js +27 -0
- package/dist/toolfactory/mcp.d.ts +37 -0
- package/dist/toolfactory/mcp.js +269 -0
- package/dist/toolfactory/types.d.ts +31 -0
- package/dist/toolfactory/types.js +10 -0
- package/dist/toolfactory/web.d.ts +4 -0
- package/dist/toolfactory/web.js +40 -0
- package/dist/validate/agent-plugins.d.ts +5 -0
- package/dist/validate/agent-plugins.js +46 -0
- package/package.json +68 -0
- package/schemas/agent-plugins/mcp.schema.json +120 -0
- package/schemas/agent-plugins/plugin.schema.json +65 -0
- package/src/bindings/index.ts +47 -0
- package/src/bindings/python.test.ts +374 -0
- package/src/bindings/python.ts +862 -0
- package/src/bindings/typescript.test.ts +329 -0
- package/src/bindings/typescript.ts +818 -0
- package/src/commands.test.ts +253 -0
- package/src/commands.ts +886 -0
- package/src/e2e.test.ts +169 -0
- package/src/hosts/browser.ts +106 -0
- package/src/hosts/github.ts +321 -0
- package/src/hosts/openclaw.ts +110 -0
- package/src/hosts/template.ts +149 -0
- package/src/hosts/web.ts +111 -0
- package/src/identity/name.test.ts +43 -0
- package/src/identity/name.ts +39 -0
- package/src/identity/read.test.ts +92 -0
- package/src/identity/read.ts +119 -0
- package/src/index.ts +11 -0
- package/src/introspect/index.ts +72 -0
- package/src/model.ts +386 -0
- package/src/ops.ts +333 -0
- package/src/project/apply.test.ts +348 -0
- package/src/project/apply.ts +417 -0
- package/src/project/gate.ts +717 -0
- package/src/project/history.ts +46 -0
- package/src/project/load.ts +104 -0
- package/src/project/lock.ts +54 -0
- package/src/project/plan.ts +101 -0
- package/src/report/coverage.test.ts +108 -0
- package/src/report/coverage.ts +113 -0
- package/src/surfaces/agent-plugins.ts +47 -0
- package/src/surfaces/agents.template.ts +32 -0
- package/src/surfaces/agents.test.ts +206 -0
- package/src/surfaces/agents.ts +729 -0
- package/src/surfaces/browser-extension.test.ts +175 -0
- package/src/surfaces/browser-extension.ts +1095 -0
- package/src/surfaces/claude.ts +99 -0
- package/src/surfaces/codex.test.ts +71 -0
- package/src/surfaces/codex.ts +105 -0
- package/src/surfaces/cursor.ts +62 -0
- package/src/surfaces/distribution.test.ts +166 -0
- package/src/surfaces/dsh.test.ts +110 -0
- package/src/surfaces/dsh.ts +159 -0
- package/src/surfaces/external.ts +13 -0
- package/src/surfaces/gemini.test.ts +80 -0
- package/src/surfaces/gemini.ts +84 -0
- package/src/surfaces/hermes-native.test.ts +179 -0
- package/src/surfaces/hermes-native.ts +584 -0
- package/src/surfaces/kernel.ts +68 -0
- package/src/surfaces/mcp-registry.test.ts +99 -0
- package/src/surfaces/mcp-registry.ts +190 -0
- package/src/surfaces/mcpb.test.ts +112 -0
- package/src/surfaces/mcpb.ts +130 -0
- package/src/surfaces/npm.ts +78 -0
- package/src/surfaces/openclaw-native.test.ts +283 -0
- package/src/surfaces/openclaw-native.ts +1070 -0
- package/src/surfaces/pypi.ts +84 -0
- package/src/surfaces/readme.ts +218 -0
- package/src/surfaces/registry.ts +79 -0
- package/src/surfaces/shared.ts +140 -0
- package/src/surfaces/skill.test.ts +103 -0
- package/src/surfaces/skill.ts +136 -0
- package/src/surfaces/web.test.ts +303 -0
- package/src/surfaces/web.ts +1310 -0
- package/src/surfaces/workflows.test.ts +677 -0
- package/src/surfaces/workflows.ts +936 -0
- package/src/toolfactory/cli.ts +74 -0
- package/src/toolfactory/config.ts +33 -0
- package/src/toolfactory/mcp.ts +311 -0
- package/src/toolfactory/types.ts +52 -0
- package/src/toolfactory/web.ts +45 -0
- package/src/validate/agent-plugins.ts +53 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-CCXhQKK3.css +2 -0
- package/web/dist/assets/index-D1PwZ3qS.js +10 -0
- 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
|
+
};
|