@toolfactory.dev/core 1.0.0-rc

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/out/codegen/access-stubs.d.ts +2 -0
  4. package/out/codegen/access-stubs.js +6 -0
  5. package/out/codegen/auth-module-names.d.ts +11 -0
  6. package/out/codegen/auth-module-names.js +34 -0
  7. package/out/codegen/auth-pipeline-render.d.ts +10 -0
  8. package/out/codegen/auth-pipeline-render.js +157 -0
  9. package/out/codegen/auth-stub-bootstrap.d.ts +42 -0
  10. package/out/codegen/auth-stub-bootstrap.js +252 -0
  11. package/out/codegen/document-validation.d.ts +22 -0
  12. package/out/codegen/document-validation.js +76 -0
  13. package/out/codegen/generated-layout.d.ts +15 -0
  14. package/out/codegen/generated-layout.js +53 -0
  15. package/out/codegen/index.d.ts +20 -0
  16. package/out/codegen/index.js +20 -0
  17. package/out/codegen/langium-cli-types.d.ts +45 -0
  18. package/out/codegen/langium-cli-types.js +1 -0
  19. package/out/codegen/logging-adapter-bootstrap.d.ts +6 -0
  20. package/out/codegen/logging-adapter-bootstrap.js +69 -0
  21. package/out/codegen/mcp-host-credential-validation.d.ts +5 -0
  22. package/out/codegen/mcp-host-credential-validation.js +15 -0
  23. package/out/codegen/mcp-host-product-runtime.d.ts +22 -0
  24. package/out/codegen/mcp-host-product-runtime.js +413 -0
  25. package/out/codegen/project-bootstrap.d.ts +29 -0
  26. package/out/codegen/project-bootstrap.js +153 -0
  27. package/out/codegen/render-http-mcp-server.d.ts +3 -0
  28. package/out/codegen/render-http-mcp-server.js +194 -0
  29. package/out/codegen/render-mcp-host-shared.d.ts +7 -0
  30. package/out/codegen/render-mcp-host-shared.js +671 -0
  31. package/out/codegen/render-oauth-http-mcp-server.d.ts +5 -0
  32. package/out/codegen/render-oauth-http-mcp-server.js +220 -0
  33. package/out/codegen/render-stdio-mcp-server.d.ts +5 -0
  34. package/out/codegen/render-stdio-mcp-server.js +58 -0
  35. package/out/codegen/write-demos-test-support.d.ts +2 -0
  36. package/out/codegen/write-demos-test-support.js +28 -0
  37. package/out/codegen/zod-codegen.d.ts +9 -0
  38. package/out/codegen/zod-codegen.js +149 -0
  39. package/out/scripts/generated-scripts-banner.d.ts +2 -0
  40. package/out/scripts/generated-scripts-banner.js +2 -0
  41. package/out/scripts/render-kill-listeners-on-port.mjs.d.ts +1 -0
  42. package/out/scripts/render-kill-listeners-on-port.mjs.js +81 -0
  43. package/out/scripts/render-load-env-local.mjs.d.ts +1 -0
  44. package/out/scripts/render-load-env-local.mjs.js +67 -0
  45. package/out/scripts/render-require-env.mjs.d.ts +1 -0
  46. package/out/scripts/render-require-env.mjs.js +36 -0
  47. package/out/scripts/write-generated-scripts.d.ts +4 -0
  48. package/out/scripts/write-generated-scripts.js +24 -0
  49. package/package.json +58 -0
@@ -0,0 +1,220 @@
1
+ import { renderMcpHostSharedSource } from './render-mcp-host-shared.js';
2
+ import { requireBaseUrlEnvArgvCheck } from './mcp-host-product-runtime.js';
3
+ /**
4
+ * Static OAuth + stateful MCP Streamable HTTP host for generated `cli/oauth-http-mcp-server.ts`.
5
+ */
6
+ export function renderOAuthHttpMcpServerSource(product = 'api2ai', loggingImport) {
7
+ const shared = renderMcpHostSharedSource('oauth-http', product);
8
+ return `#!/usr/bin/env node
9
+ /**
10
+ * Generated OAuth + stateful MCP Streamable HTTP host (static runtime — no @toolfactory.dev/core).
11
+ */
12
+ import { randomUUID } from 'node:crypto';
13
+ import * as fs from 'node:fs';
14
+ import * as http from 'node:http';
15
+ import type { IncomingMessage, ServerResponse } from 'node:http';
16
+ import * as path from 'node:path';
17
+ import { pathToFileURL } from 'node:url';
18
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
19
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
20
+ import { ListToolsRequestSchema, type ListToolsResult } from '@modelcontextprotocol/sdk/types.js';
21
+ import * as z from 'zod/v4';
22
+ import { loggingAdapter } from '${loggingImport}';
23
+
24
+ ${shared}
25
+
26
+ type SessionEntry = {
27
+ transport: StreamableHTTPServerTransport;
28
+ server: McpServer;
29
+ session: McpOAuthSession;
30
+ };
31
+
32
+ const sessionEntries = new Map<string, SessionEntry>();
33
+ const sessionStore = new Map<string, McpOAuthSession>();
34
+ const sessionHeaders = new Map<string, Record<string, string | string[] | undefined>>();
35
+
36
+ function isInitializeRequestBody(body: unknown): boolean {
37
+ if (Array.isArray(body)) {
38
+ return body.some((item) => isInitializeRequestBody(item));
39
+ }
40
+ if (!body || typeof body !== 'object') {
41
+ return false;
42
+ }
43
+ const record = body as Record<string, unknown>;
44
+ return record.jsonrpc === '2.0' && record.method === 'initialize';
45
+ }
46
+
47
+ function mcpRequiresBearerOnInitialize(generated: GeneratedHostModule): boolean {
48
+ return generated.requiresAuth && generatedHasProtectedTool(generated);
49
+ }
50
+
51
+ function readSessionId(req: IncomingMessage): string | undefined {
52
+ const raw = req.headers['mcp-session-id'];
53
+ const value = Array.isArray(raw) ? raw[0] : raw;
54
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
55
+ }
56
+
57
+ async function createMcpServerForSession(
58
+ generated: GeneratedHostModule,
59
+ httpHostConfig: OAuthHttpHostRuntimeConfig,
60
+ sessionId: string,
61
+ headers: Record<string, string | string[] | undefined>
62
+ ): Promise<SessionEntry> {
63
+ const { name, version } = requireMcpServerIdentity(generated);
64
+ const server = new McpServer({ name, version });
65
+ const session: McpOAuthSession = {
66
+ sessionId,
67
+ createdAt: Date.now()
68
+ };
69
+ sessionStore.set(sessionId, session);
70
+ await registerMcpTools(server, generated, {
71
+ envDirs: httpHostConfig.envDirs,
72
+ resolveContext: async () => {
73
+ const hdr = sessionHeaders.get(sessionId) ?? headers;
74
+ return await resolveHostContextForOAuthSession(httpHostConfig, generated, hdr, sessionStore, sessionId);
75
+ }
76
+ });
77
+ const transport = new StreamableHTTPServerTransport({
78
+ sessionIdGenerator: () => sessionId,
79
+ onsessioninitialized: (sid) => {
80
+ session.sessionId = sid;
81
+ }
82
+ });
83
+ transport.onclose = () => {
84
+ sessionEntries.delete(sessionId);
85
+ sessionStore.delete(sessionId);
86
+ sessionHeaders.delete(sessionId);
87
+ // Transport already closed — see public/passthrough HTTP host (avoid server.close loop).
88
+ };
89
+ await server.connect(transport);
90
+ return { transport, server, session };
91
+ }
92
+
93
+ async function handleOAuthMcpRequest(
94
+ req: IncomingMessage,
95
+ res: ServerResponse,
96
+ generated: GeneratedHostModule,
97
+ httpHostConfig: OAuthHttpHostRuntimeConfig
98
+ ): Promise<void> {
99
+ const headers = req.headers as Record<string, string | string[] | undefined>;
100
+ const sessionIdHeader = readSessionId(req);
101
+ const parsedBody = req.method === 'POST' ? await readMcpHttpJsonBody(req) : undefined;
102
+
103
+ if (mcpRequiresBearerOnInitialize(generated)) {
104
+ const bearer = readBearerFromHeaders(headers);
105
+ const verified = await verifyCredentialForGate(generated, bearer);
106
+ if (!verified) {
107
+ if (!sessionIdHeader && isInitializeRequestBody(parsedBody)) {
108
+ sendOAuthUnauthorized(res, httpHostConfig);
109
+ return;
110
+ }
111
+ if (sessionIdHeader && !sessionEntries.has(sessionIdHeader)) {
112
+ sendOAuthUnauthorized(res, httpHostConfig);
113
+ return;
114
+ }
115
+ }
116
+ }
117
+
118
+ let entry: SessionEntry | undefined;
119
+ if (sessionIdHeader && sessionEntries.has(sessionIdHeader)) {
120
+ entry = sessionEntries.get(sessionIdHeader);
121
+ } else if (req.method === 'POST' && isInitializeRequestBody(parsedBody)) {
122
+ const newSessionId = randomUUID();
123
+ entry = await createMcpServerForSession(generated, httpHostConfig, newSessionId, headers);
124
+ sessionEntries.set(newSessionId, entry);
125
+ } else if (sessionIdHeader) {
126
+ writeJsonRpcError(res, 404, -32_001, 'Session not found');
127
+ return;
128
+ } else if (req.method === 'POST') {
129
+ writeJsonRpcError(res, 400, -32_000, 'Bad Request: Session ID required');
130
+ return;
131
+ } else {
132
+ writeJsonRpcMethodNotAllowed(res);
133
+ return;
134
+ }
135
+
136
+ if (!entry) {
137
+ writeJsonRpcInternalError(res);
138
+ return;
139
+ }
140
+
141
+ const activeSessionId = entry.session.sessionId;
142
+ sessionHeaders.set(activeSessionId, headers);
143
+
144
+ try {
145
+ await entry.transport.handleRequest(req, res, parsedBody);
146
+ } catch (err) {
147
+ loggingAdapter.error('[mcp] oauth HTTP request failed', {
148
+ error: err instanceof Error ? err.message : String(err)
149
+ });
150
+ if (!res.headersSent) {
151
+ writeJsonRpcInternalError(res);
152
+ }
153
+ }
154
+ }
155
+
156
+ async function runOAuthHttpMcpStandaloneFromArgv(argv: string[]): Promise<void> {
157
+ const modulePath = argv[0];
158
+ if (!modulePath) {
159
+ throw new Error(
160
+ 'Usage: node oauth-http-mcp-server.js <path-to-*-tools.js> [--base-url-env ENV] --oauth-idp-url URL --port N [--oauth-scope SCOPE] [--host HOST] [--path /mcp]'
161
+ );
162
+ }
163
+ const envDirs = [process.cwd(), path.dirname(path.resolve(modulePath))];
164
+ loadLocalEnvFiles(envDirs);
165
+ const imported = await import(pathToFileURL(path.resolve(modulePath)).href);
166
+ if (!imported || typeof imported !== 'object') {
167
+ throw new Error(\`Generated module "\${modulePath}" did not export an object.\`);
168
+ }
169
+ const generated = readGeneratedModule(imported as Record<string, unknown>);
170
+ const httpHostConfig = parseOAuthHttpHostArgv(argv.slice(1), envDirs);
171
+ ${requireBaseUrlEnvArgvCheck(product, 'httpHostConfig.baseUrlEnvKey')}
172
+ await validateOAuthHttpHostAtStartup(httpHostConfig, generated);
173
+ const resourceUrl =
174
+ 'http://' + httpHostConfig.listenHost + ':' + httpHostConfig.port + httpHostConfig.mcpPath;
175
+ loggingAdapter.info('[mcp] oauth HTTP listening', {
176
+ resourceUrl,
177
+ authorizationServer: httpHostConfig.oauthIdpUrl,
178
+ oauthOnInitialize: mcpRequiresBearerOnInitialize(generated)
179
+ ? 'Bearer required (protected tools — Cursor login when enabling MCP' +
180
+ (generatedHasPublicTool(generated) ? '; public tools after login' : '') +
181
+ ')'
182
+ : 'no Bearer required (only public tools)'
183
+ });
184
+
185
+ const httpServer = http.createServer(async (req, res) => {
186
+ const url = new URL(req.url ?? '/', 'http://' + (req.headers.host ?? 'localhost'));
187
+ if (url.pathname === '/.well-known/oauth-protected-resource') {
188
+ res.writeHead(200, { 'content-type': 'application/json' });
189
+ res.end(JSON.stringify(oauthResourceMetadataDocument(httpHostConfig)));
190
+ return;
191
+ }
192
+ if (url.pathname === '/oauth/login') {
193
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
194
+ res.end(
195
+ '<!doctype html><html><body><h1>MCP OAuth</h1><p>Use Cursor MCP &quot;Needs login&quot; for PKCE OAuth, or open the IDP authorize URL from MCP logs.</p><p>IDP: ' +
196
+ httpHostConfig.oauthIdpUrl +
197
+ '/authorize</p></body></html>'
198
+ );
199
+ return;
200
+ }
201
+ if (url.pathname !== httpHostConfig.mcpPath) {
202
+ res.writeHead(404).end('Not found');
203
+ return;
204
+ }
205
+ if (req.method === 'POST' || req.method === 'GET' || req.method === 'DELETE') {
206
+ await handleOAuthMcpRequest(req, res, generated, httpHostConfig);
207
+ return;
208
+ }
209
+ res.writeHead(405).end('Method not allowed');
210
+ });
211
+
212
+ await new Promise<void>((resolve, reject) => {
213
+ httpServer.once('error', reject);
214
+ httpServer.listen(httpHostConfig.port, httpHostConfig.listenHost, () => resolve());
215
+ });
216
+ }
217
+
218
+ await runOAuthHttpMcpStandaloneFromArgv(process.argv.slice(2));
219
+ `;
220
+ }
@@ -0,0 +1,5 @@
1
+ import { type McpHostProduct } from './mcp-host-product-runtime.js';
2
+ /**
3
+ * Static MCP stdio host for generated `cli/stdio-mcp-server.ts`.
4
+ */
5
+ export declare function renderStdioMcpServerSource(product: McpHostProduct | undefined, loggingImport: string): string;
@@ -0,0 +1,58 @@
1
+ import { renderMcpHostSharedSource } from './render-mcp-host-shared.js';
2
+ import { requireBaseUrlEnvArgvCheck } from './mcp-host-product-runtime.js';
3
+ /**
4
+ * Static MCP stdio host for generated `cli/stdio-mcp-server.ts`.
5
+ */
6
+ export function renderStdioMcpServerSource(product = 'api2ai', loggingImport) {
7
+ const shared = renderMcpHostSharedSource('stdio', product);
8
+ return `#!/usr/bin/env node
9
+ /**
10
+ * Generated MCP stdio host (static runtime — no @toolfactory.dev/core).
11
+ */
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import { pathToFileURL } from 'node:url';
15
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
16
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
17
+ import { ListToolsRequestSchema, type ListToolsResult } from '@modelcontextprotocol/sdk/types.js';
18
+ import * as z from 'zod/v4';
19
+ import { loggingAdapter } from '${loggingImport}';
20
+
21
+ ${shared}
22
+
23
+ async function runStdioMcpServer(
24
+ generated: ReturnType<typeof readGeneratedModule>,
25
+ hostConfig: HostRuntimeConfig
26
+ ): Promise<void> {
27
+ const { name, version } = requireMcpServerIdentity(generated);
28
+ const server = new McpServer({ name, version });
29
+ await registerMcpTools(server, generated, {
30
+ envDirs: hostConfig.envDirs,
31
+ resolveContext: () => resolveHostContextForCall(hostConfig, generated)
32
+ });
33
+ const transport = new StdioServerTransport();
34
+ await server.connect(transport);
35
+ }
36
+
37
+ async function runStdioMcpStandaloneFromArgv(argv: string[]): Promise<void> {
38
+ const modulePath = argv[0];
39
+ if (!modulePath) {
40
+ throw new Error('Usage: node stdio-mcp-server.js <path-to-*-tools.js> [host options...]');
41
+ }
42
+ const envDirs = [process.cwd(), path.dirname(path.resolve(modulePath))];
43
+ loadLocalEnvFiles(envDirs);
44
+ const imported = await import(pathToFileURL(path.resolve(modulePath)).href);
45
+ if (!imported || typeof imported !== 'object') {
46
+ throw new Error(\`Generated module "\${modulePath}" did not export an object.\`);
47
+ }
48
+ const generated = readGeneratedModule(imported as Record<string, unknown>);
49
+ const hostConfig = parseHostArgv(argv.slice(1), envDirs);
50
+ ${requireBaseUrlEnvArgvCheck(product, 'hostConfig.baseUrlEnvKey')}
51
+ validateHostAtStartup(hostConfig, generated);
52
+ loggingAdapter.info('[mcp] host context refreshed each tool call');
53
+ await runStdioMcpServer(generated, hostConfig);
54
+ }
55
+
56
+ await runStdioMcpStandaloneFromArgv(process.argv.slice(2));
57
+ `;
58
+ }
@@ -0,0 +1,2 @@
1
+ /** Remove legacy `test/generated/*` helpers; compile check uses `npm run build:generated`. */
2
+ export declare function writeGeneratedDemosTestSupport(projectRoot: string): void;
@@ -0,0 +1,28 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { PROJECT_GENERATE_CONFIG } from '../scripts/write-generated-scripts.js';
4
+ const REMOVED_FILES = [
5
+ 'generated-module.ts',
6
+ 'mcp-stdio-smoke.ts',
7
+ 'mcp-http-smoke.ts',
8
+ 'env-helpers.ts',
9
+ 'compile-generated-fixture.ts',
10
+ 'index.ts'
11
+ ];
12
+ /** Remove legacy `test/generated/*` helpers; compile check uses `npm run build:generated`. */
13
+ export function writeGeneratedDemosTestSupport(projectRoot) {
14
+ const configPath = path.join(projectRoot, PROJECT_GENERATE_CONFIG);
15
+ if (!fs.existsSync(configPath)) {
16
+ return;
17
+ }
18
+ const outDir = path.join(projectRoot, 'test', 'generated');
19
+ for (const fileName of REMOVED_FILES) {
20
+ const stalePath = path.join(outDir, fileName);
21
+ if (fs.existsSync(stalePath)) {
22
+ fs.unlinkSync(stalePath);
23
+ }
24
+ }
25
+ if (fs.existsSync(outDir) && fs.readdirSync(outDir).length === 0) {
26
+ fs.rmdirSync(outDir);
27
+ }
28
+ }
@@ -0,0 +1,9 @@
1
+ export type JsonSchemaDict = Record<string, unknown>;
2
+ /** Emits Zod v4 expression source (uses identifier `z` in scope). */
3
+ export declare function emitZodExpression(schema: JsonSchemaDict): string;
4
+ /** Append `(example: …)` to a description when JSON Schema carries example(s) and text does not already. */
5
+ export declare function mergeJsonSchemaExampleIntoDescription(description: string | undefined, schema: JsonSchemaDict): string | undefined;
6
+ /** Shared helpers referenced by per-tool Zod schemas in generated modules. */
7
+ export declare function emitGeneratedZodPreamble(): string;
8
+ export declare function emitInputZodByToolExport(schemasByTool: Record<string, JsonSchemaDict>): string;
9
+ export declare function buildInputZodBlock(schemasByTool: Record<string, JsonSchemaDict>): string;
@@ -0,0 +1,149 @@
1
+ /** Emits Zod v4 expression source (uses identifier `z` in scope). */
2
+ export function emitZodExpression(schema) {
3
+ if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {
4
+ return 'z.unknown()';
5
+ }
6
+ if (Array.isArray(schema.anyOf)) {
7
+ return emitUnion(schema.anyOf);
8
+ }
9
+ if (Array.isArray(schema.oneOf)) {
10
+ return emitUnion(schema.oneOf);
11
+ }
12
+ if (schema.type === 'object' &&
13
+ schema.properties !== undefined &&
14
+ typeof schema.properties === 'object' &&
15
+ !Array.isArray(schema.properties)) {
16
+ const props = schema.properties;
17
+ const required = new Set(Array.isArray(schema.required)
18
+ ? schema.required.filter((x) => typeof x === 'string')
19
+ : []);
20
+ const entries = Object.entries(props).map(([key, propSchema]) => {
21
+ let inner = emitZodExpression(propSchema);
22
+ if (!required.has(key)) {
23
+ inner = `${inner}.optional()`;
24
+ }
25
+ return `${JSON.stringify(key)}: ${inner}`;
26
+ });
27
+ let obj = `z.object({ ${entries.join(', ')} })`;
28
+ if (schema.additionalProperties === false) {
29
+ obj += '.strict()';
30
+ }
31
+ return withDescribe(obj, schema);
32
+ }
33
+ if (schema.type === 'array') {
34
+ const items = emitZodExpression((schema.items ?? {}));
35
+ return withDescribe(`z.union([z.array(${items}), z.string()])`, schema);
36
+ }
37
+ if (schema.type === 'string') {
38
+ if (Array.isArray(schema.enum) && schema.enum.length >= 1 && schema.enum.every((e) => typeof e === 'string')) {
39
+ return withDescribe(emitStringPicklist(schema.enum), schema);
40
+ }
41
+ return withDescribe('z.string()', schema);
42
+ }
43
+ if (schema.type === 'number' || schema.type === 'integer') {
44
+ return emitLlmTolerantNumber(schema, schema.type === 'integer');
45
+ }
46
+ if (schema.type === 'boolean') {
47
+ return withDescribe('z.union([z.boolean(), z.literal("true"), z.literal("false")])', schema);
48
+ }
49
+ if (schema.type === 'object' && schema.additionalProperties === true) {
50
+ return withDescribe('z.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))', schema);
51
+ }
52
+ if (schema.type === 'object' &&
53
+ typeof schema.additionalProperties === 'object' &&
54
+ schema.additionalProperties !== null &&
55
+ !Array.isArray(schema.additionalProperties)) {
56
+ const valueType = emitZodExpression(schema.additionalProperties);
57
+ return withDescribe(`z.record(z.string(), ${valueType})`, schema);
58
+ }
59
+ return 'z.unknown()';
60
+ }
61
+ function formatExampleForDescription(value) {
62
+ if (typeof value === 'string') {
63
+ return value;
64
+ }
65
+ if (typeof value === 'number' || typeof value === 'boolean') {
66
+ return String(value);
67
+ }
68
+ return JSON.stringify(value);
69
+ }
70
+ function firstJsonSchemaExampleValue(schema) {
71
+ if (Array.isArray(schema.examples) && schema.examples.length > 0) {
72
+ return schema.examples[0];
73
+ }
74
+ if (schema.example !== undefined) {
75
+ return schema.example;
76
+ }
77
+ return undefined;
78
+ }
79
+ /** Append `(example: …)` to a description when JSON Schema carries example(s) and text does not already. */
80
+ export function mergeJsonSchemaExampleIntoDescription(description, schema) {
81
+ const trimmed = description?.trim() ?? '';
82
+ if (trimmed.includes('(example:')) {
83
+ return trimmed.length > 0 ? trimmed : undefined;
84
+ }
85
+ const exampleValue = firstJsonSchemaExampleValue(schema);
86
+ if (exampleValue === undefined) {
87
+ return trimmed.length > 0 ? trimmed : undefined;
88
+ }
89
+ const suffix = `(example: ${formatExampleForDescription(exampleValue)})`;
90
+ return trimmed.length > 0 ? `${trimmed} ${suffix}` : suffix;
91
+ }
92
+ function withDescribe(expr, schema) {
93
+ const desc = mergeJsonSchemaExampleIntoDescription(typeof schema.description === 'string' ? schema.description : undefined, schema);
94
+ if (desc && desc.length > 0) {
95
+ return `${expr}.describe(${JSON.stringify(desc)})`;
96
+ }
97
+ return expr;
98
+ }
99
+ function emitUnion(parts) {
100
+ const emitted = parts.map((p) => emitZodExpression(p));
101
+ if (emitted.length === 0) {
102
+ return 'z.never()';
103
+ }
104
+ if (emitted.length === 1) {
105
+ return emitted[0];
106
+ }
107
+ return `z.union([${emitted.join(', ')}])`;
108
+ }
109
+ function emitStringPicklist(strings) {
110
+ if (strings.length === 0) {
111
+ return 'z.never()';
112
+ }
113
+ if (strings.length === 1) {
114
+ return `z.literal(${JSON.stringify(strings[0])})`;
115
+ }
116
+ return `z.union([${strings.map((v) => `z.literal(${JSON.stringify(v)})`).join(', ')}])`;
117
+ }
118
+ /** MCP tool args: models often pass OpenAPI numbers/booleans as JSON strings. */
119
+ function emitLlmTolerantNumber(schema, integer) {
120
+ if (Array.isArray(schema.enum) && schema.enum.length >= 1 && schema.enum.every(isFiniteNumber)) {
121
+ const literals = schema.enum.flatMap((v) => [
122
+ `z.literal(${v})`,
123
+ `z.literal(${JSON.stringify(String(v))})`
124
+ ]);
125
+ if (literals.length === 1) {
126
+ return withDescribe(literals[0], schema);
127
+ }
128
+ return withDescribe(`z.union([${literals.join(', ')}])`, schema);
129
+ }
130
+ const numberBranch = integer ? 'z.number().int()' : 'z.number()';
131
+ return withDescribe(`z.union([${numberBranch}, z.string()])`, schema);
132
+ }
133
+ function isFiniteNumber(value) {
134
+ return typeof value === 'number' && Number.isFinite(value);
135
+ }
136
+ /** Shared helpers referenced by per-tool Zod schemas in generated modules. */
137
+ export function emitGeneratedZodPreamble() {
138
+ return `import * as z from 'zod/v4';
139
+ `;
140
+ }
141
+ export function emitInputZodByToolExport(schemasByTool) {
142
+ const entries = Object.entries(schemasByTool).map(([toolName, schema]) => {
143
+ return ` ${JSON.stringify(toolName)}: ${emitZodExpression(schema)}`;
144
+ });
145
+ return `export const inputZodByTool = {\n${entries.join(',\n')}\n};`;
146
+ }
147
+ export function buildInputZodBlock(schemasByTool) {
148
+ return `${emitInputZodByToolExport(schemasByTool)}\n`;
149
+ }
@@ -0,0 +1,2 @@
1
+ /** Prepended to every file under consumer `scripts/generated/`. */
2
+ export declare const GENERATED_SCRIPTS_BANNER = "// @generated from @toolfactory.dev/core \u2014 do not edit; regenerated when running project generate.\n\n";
@@ -0,0 +1,2 @@
1
+ /** Prepended to every file under consumer `scripts/generated/`. */
2
+ export const GENERATED_SCRIPTS_BANNER = '// @generated from @toolfactory.dev/core — do not edit; regenerated when running project generate.\n\n';
@@ -0,0 +1 @@
1
+ export declare function renderKillListenersOnPortMjsSource(): string;
@@ -0,0 +1,81 @@
1
+ import { GENERATED_SCRIPTS_BANNER } from './generated-scripts-banner.js';
2
+ export function renderKillListenersOnPortMjsSource() {
3
+ return `${GENERATED_SCRIPTS_BANNER}/**
4
+ * Kill only TCP listeners on a port (not clients connected to that port).
5
+ */
6
+ import { execSync } from 'node:child_process';
7
+
8
+ /**
9
+ * @param {number} port
10
+ * @returns {string[]}
11
+ */
12
+ function listListenerPids(port) {
13
+ try {
14
+ const raw = execSync(\`lsof -nP -iTCP:\${port} -sTCP:LISTEN -t\`, { encoding: 'utf8' }).trim();
15
+ if (!raw) {
16
+ return [];
17
+ }
18
+ return raw.split('\\n').filter(Boolean);
19
+ } catch (err) {
20
+ const status = err && typeof err === 'object' && 'status' in err ? err.status : undefined;
21
+ if (status === 1) {
22
+ return [];
23
+ }
24
+ throw err;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * @param {string} pid
30
+ * @returns {string}
31
+ */
32
+ function processCommand(pid) {
33
+ try {
34
+ return execSync(\`ps -p \${pid} -o comm=\`, { encoding: 'utf8' }).trim();
35
+ } catch {
36
+ return '';
37
+ }
38
+ }
39
+
40
+ function isNodeListener(pid) {
41
+ const comm = processCommand(pid).toLowerCase();
42
+ return comm === 'node' || comm.endsWith('/node');
43
+ }
44
+
45
+ /**
46
+ * @param {number} port
47
+ * @param {{ logPrefix?: string, nodeOnly?: boolean }} [options]
48
+ * @returns {{ killed: string[], skipped: string[] }}
49
+ */
50
+ export function killListenersOnPort(port, options = {}) {
51
+ const logPrefix = options.logPrefix ?? 'kill';
52
+ const nodeOnly = options.nodeOnly !== false;
53
+ const pids = listListenerPids(port);
54
+ if (pids.length === 0) {
55
+ console.log(\`[\${logPrefix}] port \${port}: nothing listening\`);
56
+ return { killed: [], skipped: [] };
57
+ }
58
+
59
+ const killed = [];
60
+ const skipped = [];
61
+ for (const pid of pids) {
62
+ if (nodeOnly && !isNodeListener(pid)) {
63
+ const comm = processCommand(pid) || 'unknown';
64
+ skipped.push(pid);
65
+ console.warn(\`[\${logPrefix}] port \${port}: skip pid \${pid} (\${comm}) — not a node listener\`);
66
+ continue;
67
+ }
68
+ execSync(\`kill \${pid}\`);
69
+ killed.push(pid);
70
+ }
71
+
72
+ if (killed.length > 0) {
73
+ console.log(\`[\${logPrefix}] port \${port}: stopped \${killed.join(', ')}\`);
74
+ }
75
+ if (killed.length === 0 && skipped.length > 0) {
76
+ console.log(\`[\${logPrefix}] port \${port}: no node listener stopped\`);
77
+ }
78
+ return { killed, skipped };
79
+ }
80
+ `;
81
+ }
@@ -0,0 +1 @@
1
+ export declare function renderLoadEnvLocalMjsSource(): string;
@@ -0,0 +1,67 @@
1
+ import { GENERATED_SCRIPTS_BANNER } from './generated-scripts-banner.js';
2
+ export function renderLoadEnvLocalMjsSource() {
3
+ return `${GENERATED_SCRIPTS_BANNER}/**
4
+ * Load \`.env\` then \`.env.local\` into process.env.
5
+ * Does not override keys already set in the shell; \`.env.local\` overrides \`.env\`.
6
+ */
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const defaultRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
12
+
13
+ function stripOptionalQuotes(value) {
14
+ if (value.length < 2) {
15
+ return value;
16
+ }
17
+ const first = value[0];
18
+ const last = value[value.length - 1];
19
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
20
+ return value.slice(1, -1);
21
+ }
22
+ return value;
23
+ }
24
+
25
+ function parseEnvLine(line) {
26
+ const trimmed = line.trim();
27
+ if (trimmed.length === 0 || trimmed.startsWith('#')) {
28
+ return undefined;
29
+ }
30
+ const assignment = trimmed.startsWith('export ') ? trimmed.slice('export '.length).trim() : trimmed;
31
+ const separator = assignment.indexOf('=');
32
+ if (separator <= 0) {
33
+ return undefined;
34
+ }
35
+ const key = assignment.slice(0, separator).trim();
36
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
37
+ return undefined;
38
+ }
39
+ return [key, stripOptionalQuotes(assignment.slice(separator + 1).trim())];
40
+ }
41
+
42
+ function loadEnvFile(filePath, options) {
43
+ const overrideExisting = options?.overrideExisting === true;
44
+ if (!existsSync(filePath)) {
45
+ return false;
46
+ }
47
+ const content = readFileSync(filePath, 'utf-8');
48
+ for (const line of content.split(/\\r?\\n/u)) {
49
+ const parsed = parseEnvLine(line);
50
+ if (!parsed) {
51
+ continue;
52
+ }
53
+ const [key, value] = parsed;
54
+ if (overrideExisting || process.env[key] === undefined) {
55
+ process.env[key] = value;
56
+ }
57
+ }
58
+ return true;
59
+ }
60
+
61
+ /** @param {string} [root] defaults to project root (parent of scripts/) */
62
+ export function loadProjectEnvLocal(root = defaultRoot) {
63
+ loadEnvFile(path.join(root, '.env'));
64
+ loadEnvFile(path.join(root, '.env.local'), { overrideExisting: true });
65
+ }
66
+ `;
67
+ }
@@ -0,0 +1 @@
1
+ export declare function renderRequireEnvMjsSource(): string;