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 @@
1
+ export {};
@@ -0,0 +1,316 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { createServer as createNetServer } from "node:http";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { describe, expect, it } from "vitest";
7
+ import { listTools } from "../introspect/index.js";
8
+ import { ToolConfigSchema } from "../model.js";
9
+ import { surface as pypi } from "../surfaces/pypi.js";
10
+ import { cliCommand, cli as cliFiles, kernel, kernelCommand, liveTest, scaffold, } from "./python.js";
11
+ function project(surfaces, overrides = {}) {
12
+ return {
13
+ root: "/tmp/none",
14
+ tool: ToolConfigSchema.parse({
15
+ schemaVersion: 1,
16
+ identity: "plugin.json",
17
+ binding: "python",
18
+ surfaces,
19
+ config: { type: "object", properties: { apiKey: { type: "string" } } },
20
+ }),
21
+ identity: {
22
+ name: "hello.py",
23
+ version: "0.2.0",
24
+ description: "Say hello.",
25
+ license: "MIT",
26
+ repository: "https://github.com/acme/hello",
27
+ author: { name: "Ada", email: "ada@example.com" },
28
+ },
29
+ identityExtra: {},
30
+ operations: [],
31
+ toolfactoryVersion: "0.1.0",
32
+ ...overrides,
33
+ };
34
+ }
35
+ /** A project whose config declares one required, sensitive credential: what gates the T4 tier. */
36
+ function withCredential(surfaces) {
37
+ const base = project(surfaces);
38
+ base.tool.config = {
39
+ type: "object",
40
+ properties: { passkey: { type: "string", "x-toolfactory": { sensitive: true } } },
41
+ required: ["passkey"],
42
+ };
43
+ base.tool.tests = { examples: { echo: { text: "hi" } } };
44
+ base.operations = [{ name: "echo", inputSchema: { type: "object" }, requires: [] }];
45
+ return base;
46
+ }
47
+ const paths = (files) => files.map((file) => file.path);
48
+ const text = (files, path) => files.find((file) => file.path === path).content;
49
+ describe("python binding", () => {
50
+ it("names every artifact from the canonical name", () => {
51
+ const both = project(["mcp", "cli"]);
52
+ expect(kernelCommand(both).args).toEqual([
53
+ "run",
54
+ "--quiet",
55
+ "python",
56
+ "-m",
57
+ "hello_py.toolfactory.mcp",
58
+ ]);
59
+ expect(cliCommand(both).args.at(-1)).toBe("hello_py.toolfactory.cli");
60
+ expect(paths([...kernel(both), ...cliFiles(both)])).toEqual([
61
+ "src/hello_py/toolfactory/__init__.py",
62
+ "src/hello_py/toolfactory/types.py",
63
+ "src/hello_py/toolfactory/config.py",
64
+ "src/hello_py/toolfactory/mcp.py",
65
+ "src/hello_py/toolfactory/cli.py",
66
+ ]);
67
+ // A file exists iff a selected surface owns it, and config keys become environment reads.
68
+ expect(paths(kernel(project(["mcp"])))).not.toContain("src/hello_py/toolfactory/cli.py");
69
+ expect(text(kernel(both), "src/hello_py/toolfactory/config.py")).toContain('"apiKey": os.environ.get("APIKEY")');
70
+ expect(text(scaffold(both), "pyproject.toml")).toContain('"hello.py" = "hello_py.toolfactory.cli:main"');
71
+ });
72
+ it("reads the data directory from <N>_DATA_DIR, else the platform default", () => {
73
+ const files = kernel(project(["mcp"]));
74
+ expect(text(files, "src/hello_py/toolfactory/types.py")).toContain("data_dir: Path");
75
+ const config = text(files, "src/hello_py/toolfactory/config.py");
76
+ expect(config).toContain('os.environ.get("HELLO_PY_DATA_DIR")');
77
+ expect(config).toContain('os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share"');
78
+ expect(config).toContain('os.environ.get("LOCALAPPDATA")');
79
+ expect(config).toContain('return base / "hello.py"');
80
+ expect(config).toContain("data_dir=data_dir()");
81
+ });
82
+ it("emits the T4 live test only for a required sensitive credential", () => {
83
+ expect(liveTest(project(["cli"]))).toEqual([]);
84
+ const live = liveTest(withCredential(["cli"]))[0];
85
+ if (live?.kind !== "region")
86
+ throw new Error("expected a region file");
87
+ expect(live.path).toBe("tests/test_live.py");
88
+ expect(live.regions[0]?.content).toContain('CREDENTIALS = ("PASSKEY",)');
89
+ expect(live.regions[0]?.content).toContain("LIVE = all(os.environ.get(name)");
90
+ expect(live.template).toContain("@pytest.mark.skipif(not LIVE");
91
+ // pydantic's own JSON entry point, so tool.json's example arguments need no translation.
92
+ expect(live.template).toContain('operation.input.model_validate_json("{\\"text\\":\\"hi\\"}")');
93
+ // The header documents the local command; CI runs the same one without --env-file.
94
+ expect(live.template).toContain("uv run --env-file .env --with pytest pytest -q tests/test_live.py");
95
+ });
96
+ it("emits opt-in --http and --pair flags, defaulting to stdio, on the mcp module and the cli", () => {
97
+ const files = [...kernel(project(["mcp", "cli"])), ...cliFiles(project(["mcp", "cli"]))];
98
+ const mcp = text(files, "src/hello_py/toolfactory/mcp.py");
99
+ expect(mcp).toContain("def serve_http(");
100
+ // One ASGI app on both paths: the SDK's own streamable-HTTP app, run by uvicorn over a
101
+ // socket bound first, so `--http 0` can report the port it actually got.
102
+ expect(mcp).toContain("app = server.streamable_http_app(");
103
+ expect(mcp).toContain("sock = config.bind_socket()");
104
+ expect(mcp).toContain("uvicorn.Server(config).run(sockets=[sock])");
105
+ expect(mcp).toContain("Serving MCP streamable HTTP on {base}{path}{suffix}");
106
+ expect(mcp).toContain("stateless_http=True");
107
+ // Stdio is still the default: the standalone entrypoint only switches transport when --http is present.
108
+ expect(mcp).toContain("if args.http is None and not args.pair:");
109
+ // The pairing token: `<N>_MCP_TOKEN` or a `relay-token` file under the data directory, and
110
+ // `--pair` to mint one. Neither present means no token and no behaviour change.
111
+ expect(mcp).toContain('os.environ.get("HELLO_PY_MCP_TOKEN")');
112
+ expect(mcp).toContain("data_dir() / TOKEN_FILE");
113
+ expect(mcp).toContain("hmac.compare_digest(hashlib.sha256(presented).digest(), expected)");
114
+ const cli = text(files, "src/hello_py/toolfactory/cli.py");
115
+ expect(cli).toContain('"--http"');
116
+ expect(cli).toContain("from .mcp import serve_http");
117
+ expect(cli).toContain("if options.http is not None or options.pair:");
118
+ expect(cli).toContain('"--pair"');
119
+ // Without the `web` surface there is no page to serve, open or write secrets for.
120
+ expect(mcp).not.toContain("web_route()");
121
+ expect(mcp).not.toContain("SECRETS");
122
+ expect(paths(kernel(project(["mcp", "cli"])))).not.toContain("src/hello_py/toolfactory/web.py");
123
+ });
124
+ it("serves the page, opens it and takes a secret when the web surface is selected", () => {
125
+ const target = withCredential(["mcp", "cli", "web"]);
126
+ const files = [...kernel(target), ...cliFiles(target)];
127
+ const mcp = text(files, "src/hello_py/toolfactory/mcp.py");
128
+ // The static half: the wheel's own copy first, then this checkout's build output.
129
+ expect(mcp).toContain('for candidate in (package / "web", package.parent.parent / "web" / "dist")');
130
+ expect(mcp).toContain('Mount("/", app=StaticFiles(directory=directory, html=True))');
131
+ expect(mcp).toContain("run `npm -C web run build`");
132
+ // The token guards the API paths only — a browser cannot put a header on its own document.
133
+ expect(mcp).toContain('app = guard(app, token, {path, "/env"})');
134
+ expect(mcp).toContain('SECRETS = ["PASSKEY"]');
135
+ expect(mcp).toContain('return JSONResponse({"declared": SECRETS, "present": env_present()})');
136
+ expect(mcp).toContain("env_path().chmod(0o600)");
137
+ expect(mcp).toContain('print(f".env: set {name}", file=sys.stderr, flush=True)');
138
+ expect(mcp).toContain('webbrowser.open(f"{base}/#{token}")');
139
+ expect(mcp).toContain("mint_token() if open_browser else None");
140
+ // The `web` operation: a detached child holds the socket, never this process.
141
+ const web = text(files, "src/hello_py/toolfactory/web.py");
142
+ expect(web).toContain('[sys.executable, "-m", f"{__package__}.mcp", "--http", "0", "--open"]');
143
+ expect(web).toContain("start_new_session=True");
144
+ expect(web).toContain('SERVING = re.compile(r"Serving MCP streamable HTTP on (\\S+)")');
145
+ expect(mcp).toContain("OPERATIONS = [*AUTHORED_OPERATIONS, WEB_OPERATION]");
146
+ expect(text(files, "src/hello_py/toolfactory/cli.py")).toContain("OPERATIONS = [*AUTHORED_OPERATIONS, WEB_OPERATION]");
147
+ });
148
+ it("projects identity into pyproject.toml only when it is not the identity file", () => {
149
+ const patch = (p) => pypi.plan(p)[0].patch.project;
150
+ expect(patch(project(["pypi", "cli"]))).toMatchObject({
151
+ name: "hello-py",
152
+ version: "0.2.0",
153
+ authors: [{ name: "Ada", email: "ada@example.com" }],
154
+ urls: { Repository: "https://github.com/acme/hello" },
155
+ scripts: { "hello.py": "hello_py.toolfactory.cli:main" },
156
+ });
157
+ const authored = project(["pypi", "cli"]);
158
+ authored.tool.identity = "pyproject.toml";
159
+ expect(patch(authored)).toEqual({ scripts: { "hello.py": "hello_py.toolfactory.cli:main" } });
160
+ });
161
+ it("carries the MCP Registry ownership marker in the README", () => {
162
+ const files = pypi.plan(project(["pypi", "mcp-registry"]));
163
+ expect(files[0].patch).toMatchObject({ project: { readme: "README.md" } });
164
+ const readme = files[1];
165
+ if (readme.kind !== "region")
166
+ throw new Error("expected a region file");
167
+ expect(readme.regions[0]?.content).toContain("<!-- mcp-name: io.github.acme/hello.py -->");
168
+ });
169
+ });
170
+ const uv = spawnSync("uv", ["--version"], { encoding: "utf8" }).status === 0;
171
+ describe.skipIf(!uv)("python kernel, really run", () => {
172
+ it("serves tools/list and answers the CLI", { timeout: 300_000 }, async () => {
173
+ const root = mkdtempSync(join(tmpdir(), "toolfactory-python-"));
174
+ const real = project(["mcp", "cli"], { root, identity: { name: "probe", version: "0.1.0" } });
175
+ for (const file of [...scaffold(real), ...kernel(real), ...cliFiles(real)]) {
176
+ const path = join(root, file.path);
177
+ mkdirSync(dirname(path), { recursive: true });
178
+ writeFileSync(path, file.content);
179
+ }
180
+ const sync = spawnSync("uv", ["sync", "--quiet"], { cwd: root, encoding: "utf8" });
181
+ expect(sync.status, sync.stderr).toBe(0);
182
+ const { command, args } = kernelCommand(real);
183
+ const ops = await listTools(root, command, args);
184
+ expect(ops.tools[0]).toMatchObject({
185
+ name: "echo",
186
+ inputSchema: { required: ["text"] },
187
+ outputSchema: { required: ["text"] },
188
+ _meta: { "dev.toolfactory": { requires: [] } },
189
+ });
190
+ const cli = cliCommand(real);
191
+ const ran = spawnSync(cli.command, [...cli.args, "echo", "--text", "hi"], {
192
+ cwd: root,
193
+ encoding: "utf8",
194
+ });
195
+ expect(JSON.parse(ran.stdout)).toEqual({ text: "hi" });
196
+ });
197
+ it("serves tools/list over --http, and requires the token minted by --pair", {
198
+ timeout: 300_000,
199
+ }, async () => {
200
+ const root = mkdtempSync(join(tmpdir(), "toolfactory-python-http-"));
201
+ const real = project(["mcp", "cli"], { root, identity: { name: "probe", version: "0.1.0" } });
202
+ for (const file of [...scaffold(real), ...kernel(real), ...cliFiles(real)]) {
203
+ const path = join(root, file.path);
204
+ mkdirSync(dirname(path), { recursive: true });
205
+ writeFileSync(path, file.content);
206
+ }
207
+ const sync = spawnSync("uv", ["sync", "--quiet"], { cwd: root, encoding: "utf8" });
208
+ expect(sync.status, sync.stderr).toBe(0);
209
+ const dataDir = join(root, "data");
210
+ const env = { ...process.env, PROBE_DATA_DIR: dataDir };
211
+ const port = await freePort();
212
+ const cli = cliCommand(real);
213
+ const child = spawn(cli.command, [...cli.args, "mcp", "--http", String(port)], {
214
+ cwd: root,
215
+ env,
216
+ stdio: ["ignore", "pipe", "pipe"],
217
+ });
218
+ const stderr = [];
219
+ child.stderr?.on("data", (chunk) => stderr.push(String(chunk)));
220
+ try {
221
+ const url = `http://127.0.0.1:${port}/mcp`;
222
+ const init = await waitForInitialize(url, 30_000).catch((error) => {
223
+ throw new Error(`${error instanceof Error ? error.message : error}\n${stderr.join("")}`);
224
+ });
225
+ expect(init.status).toBe(200);
226
+ const negotiated = init.json.result
227
+ .protocolVersion;
228
+ const list = await postRpc(url, { jsonrpc: "2.0", id: 2, method: "tools/list" }, negotiated);
229
+ expect(list.status).toBe(200);
230
+ const tools = list.json.result.tools;
231
+ expect(tools.map((tool) => tool.name)).toEqual(["echo"]);
232
+ }
233
+ finally {
234
+ child.kill();
235
+ }
236
+ // The same server started with `--pair`: it prints the pairing string the extension's options
237
+ // page takes, keeps the token under the data directory, and refuses a request without it.
238
+ const pairPort = await freePort();
239
+ const paired = spawn(cli.command, [...cli.args, "mcp", "--http", String(pairPort), "--pair"], {
240
+ cwd: root,
241
+ env,
242
+ stdio: ["ignore", "pipe", "pipe"],
243
+ });
244
+ const stdout = [];
245
+ paired.stdout?.on("data", (chunk) => stdout.push(String(chunk)));
246
+ try {
247
+ const url = `http://127.0.0.1:${pairPort}/mcp`;
248
+ const anonymous = await waitForInitialize(url, 30_000);
249
+ expect(anonymous.status).toBe(401);
250
+ const [printed, token] = stdout.join("").trim().split("\n")[0]?.split("#") ?? [];
251
+ expect(printed).toBe(url);
252
+ expect(readFileSync(join(dataDir, "relay-token"), "utf8").trim()).toBe(token);
253
+ const init = await waitForInitialize(url, 30_000, token);
254
+ expect(init.status).toBe(200);
255
+ }
256
+ finally {
257
+ paired.kill();
258
+ }
259
+ });
260
+ });
261
+ function freePort() {
262
+ return new Promise((resolve, reject) => {
263
+ const probe = createNetServer();
264
+ probe.on("error", reject);
265
+ probe.listen(0, "127.0.0.1", () => {
266
+ const address = probe.address();
267
+ const port = typeof address === "object" && address ? address.port : 0;
268
+ probe.close((error) => (error ? reject(error) : resolve(port)));
269
+ });
270
+ });
271
+ }
272
+ // The streamable HTTP transport requires `Accept: application/json, text/event-stream` on every
273
+ // POST, and `MCP-Protocol-Version` on every request *after* the one that negotiates it in `initialize`.
274
+ async function postRpc(url, body, protocolVersion, token) {
275
+ const headers = {
276
+ "Content-Type": "application/json",
277
+ Accept: "application/json, text/event-stream",
278
+ };
279
+ if (protocolVersion)
280
+ headers["MCP-Protocol-Version"] = protocolVersion;
281
+ if (token)
282
+ headers.Authorization = `Bearer ${token}`;
283
+ const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
284
+ const raw = await response.text();
285
+ const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
286
+ const payload = isEventStream
287
+ ? JSON.parse(raw
288
+ .split("\n")
289
+ .find((line) => line.startsWith("data: "))
290
+ ?.slice("data: ".length) ?? "{}")
291
+ : JSON.parse(raw || "{}");
292
+ return { status: response.status, json: payload };
293
+ }
294
+ /** Retries `initialize` until the freshly-spawned server starts accepting connections. */
295
+ async function waitForInitialize(url, deadlineMs, token) {
296
+ const deadline = Date.now() + deadlineMs;
297
+ for (;;) {
298
+ try {
299
+ return await postRpc(url, {
300
+ jsonrpc: "2.0",
301
+ id: 1,
302
+ method: "initialize",
303
+ params: {
304
+ protocolVersion: "2026-07-28",
305
+ capabilities: {},
306
+ clientInfo: { name: "toolfactory-test", version: "0.0.0" },
307
+ },
308
+ }, undefined, token);
309
+ }
310
+ catch (error) {
311
+ if (Date.now() >= deadline)
312
+ throw error;
313
+ await new Promise((resolve) => setTimeout(resolve, 250));
314
+ }
315
+ }
316
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * TypeScript binding: the six language-specific things, as templates against upstream
3
+ * SDKs only. The kernel files under `src/toolfactory/` are regenerated on every build; the
4
+ * author's `src/ops.ts` is scaffolded once and then owned.
5
+ */
6
+ import type { PlannedFile, Project } from "../model.js";
7
+ export declare const KERNEL_DIR = "src/toolfactory";
8
+ export declare const OPS_PATH = "src/ops.ts";
9
+ export declare const LIVE_TEST_PATH = "tests/live.test.ts";
10
+ /**
11
+ * The scaffold script the `live` CI job and the author both run (§6 T4). Node's own
12
+ * `--env-file`, so no dotenv dependency (vitest does not put `.env` into `process.env`);
13
+ * `-if-exists` so CI, which injects the same names from the `live-tests` environment rather
14
+ * than a file, runs the same script; vitest's own entry file rather than `node_modules/.bin`,
15
+ * which is a shell shim under pnpm and cannot be handed to `node`.
16
+ */
17
+ export declare const LIVE_TEST_SCRIPT = "node --env-file-if-exists=.env node_modules/vitest/vitest.mjs run tests/live.test.ts";
18
+ export declare function kernelCommand(): {
19
+ command: string;
20
+ args: string[];
21
+ };
22
+ export declare function cliCommand(): {
23
+ command: string;
24
+ args: string[];
25
+ };
26
+ /**
27
+ * The T4 live tier: real calls against the real service, kept out of the default `vitest run`
28
+ * by a generated guard over the credentials `tool.json` declares as required and sensitive.
29
+ * The markers hold only the guard; the example test around them is the author's.
30
+ */
31
+ export declare function liveTest(project: Project): PlannedFile[];
32
+ /** The kernel, generated on every build for every tool: the contract, config, and the MCP server toolfactory introspects. */
33
+ export declare function kernel(project: Project): PlannedFile[];
34
+ /** The CLI surface's file. */
35
+ export declare function cli(project: Project): PlannedFile[];
36
+ /** The Node floor every generated TypeScript project declares (vite 8 and the MCP SDK need it). */
37
+ export declare const NODE_ENGINES = ">=22.12";
38
+ /** Written by `init` only when absent; owned by the author afterwards. */
39
+ export declare function scaffold(project: Project): PlannedFile[];