@amalgm/tools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/PURPOSE.md +87 -0
  2. package/README.md +62 -0
  3. package/dist/api-driver.d.ts +3 -0
  4. package/dist/api-driver.js +92 -0
  5. package/dist/artifact-files.d.ts +28 -0
  6. package/dist/artifact-files.js +115 -0
  7. package/dist/artifacts.d.ts +66 -0
  8. package/dist/artifacts.js +112 -0
  9. package/dist/bin/mcp.d.ts +2 -0
  10. package/dist/bin/mcp.js +13 -0
  11. package/dist/bin/tools.d.ts +2 -0
  12. package/dist/bin/tools.js +3 -0
  13. package/dist/cli-driver.d.ts +3 -0
  14. package/dist/cli-driver.js +52 -0
  15. package/dist/cli.d.ts +13 -0
  16. package/dist/cli.js +123 -0
  17. package/dist/definition.d.ts +7 -0
  18. package/dist/definition.js +170 -0
  19. package/dist/ids.d.ts +7 -0
  20. package/dist/ids.js +38 -0
  21. package/dist/index.d.ts +9 -0
  22. package/dist/index.js +7 -0
  23. package/dist/input.d.ts +4 -0
  24. package/dist/input.js +43 -0
  25. package/dist/mcp-server.d.ts +18 -0
  26. package/dist/mcp-server.js +61 -0
  27. package/dist/mcp.d.ts +9 -0
  28. package/dist/mcp.js +95 -0
  29. package/dist/module.d.ts +3 -0
  30. package/dist/module.js +22 -0
  31. package/dist/process.d.ts +22 -0
  32. package/dist/process.js +46 -0
  33. package/dist/query.d.ts +23 -0
  34. package/dist/query.js +36 -0
  35. package/dist/results.d.ts +6 -0
  36. package/dist/results.js +27 -0
  37. package/dist/schema.d.ts +3 -0
  38. package/dist/schema.js +26 -0
  39. package/dist/secrets.d.ts +3 -0
  40. package/dist/secrets.js +13 -0
  41. package/dist/selection.d.ts +10 -0
  42. package/dist/selection.js +18 -0
  43. package/dist/store.d.ts +17 -0
  44. package/dist/store.js +96 -0
  45. package/dist/toolbox.d.ts +46 -0
  46. package/dist/toolbox.js +204 -0
  47. package/dist/types.d.ts +172 -0
  48. package/dist/types.js +1 -0
  49. package/dist/updates.d.ts +7 -0
  50. package/dist/updates.js +57 -0
  51. package/docs/ENGINE_INTEGRATION.md +57 -0
  52. package/package.json +57 -0
package/dist/cli.js ADDED
@@ -0,0 +1,123 @@
1
+ import { loadDefinitions } from './module.js';
2
+ import { Toolbox } from './toolbox.js';
3
+ const HELP = `Usage: amalgm-tools [--state-dir DIR] [--loadout IDS] <command>
4
+
5
+ Commands:
6
+ apply <definition.js|json> Apply one tool or an array of tools
7
+ list List tools and enabled actions
8
+ catalog Print the complete canonical catalog
9
+ show <tool-or-action-id> Show one record
10
+ enable <tool-or-action-id> Enable one record
11
+ disable <tool-or-action-id> Disable one record
12
+ remove <tool-or-action-id> Remove one record
13
+ run <action-id> [--input JSON]
14
+ Run an action
15
+ connections List MCP connections for a composing host`;
16
+ function parse(argv) {
17
+ const args = [...argv];
18
+ let stateDir;
19
+ let loadout;
20
+ let input = {};
21
+ for (let index = 0; index < args.length;) {
22
+ const flag = args[index];
23
+ if (flag === '--state-dir') {
24
+ if (!args[index + 1])
25
+ throw new Error('--state-dir requires a value');
26
+ stateDir = args[index + 1];
27
+ args.splice(index, 2);
28
+ }
29
+ else if (flag === '--loadout') {
30
+ if (args[index + 1] === undefined)
31
+ throw new Error('--loadout requires a comma-separated value');
32
+ loadout = args[index + 1].split(',').map((value) => value.trim()).filter(Boolean);
33
+ args.splice(index, 2);
34
+ }
35
+ else if (flag === '--input') {
36
+ if (!args[index + 1])
37
+ throw new Error('--input requires JSON');
38
+ input = JSON.parse(args[index + 1]);
39
+ args.splice(index, 2);
40
+ }
41
+ else
42
+ index += 1;
43
+ }
44
+ return { command: args.shift(), args, stateDir, loadout, input };
45
+ }
46
+ function required(value, label) {
47
+ if (!value)
48
+ throw new Error(`${label} is required`);
49
+ return value;
50
+ }
51
+ function write(output, value) {
52
+ output.write(`${JSON.stringify(value, null, 2)}\n`);
53
+ }
54
+ async function runCli(argv, io = {}) {
55
+ const stdout = io.stdout || process.stdout;
56
+ const stderr = io.stderr || process.stderr;
57
+ let parsed;
58
+ try {
59
+ parsed = parse(argv);
60
+ }
61
+ catch (error) {
62
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
63
+ return 1;
64
+ }
65
+ if (!parsed.command || ['help', '-h', '--help'].includes(parsed.command)) {
66
+ stdout.write(`${HELP}\n`);
67
+ return 0;
68
+ }
69
+ const toolbox = io.toolbox || new Toolbox({ stateDir: parsed.stateDir });
70
+ try {
71
+ const recordId = parsed.args[0];
72
+ switch (parsed.command) {
73
+ case 'apply': {
74
+ const definitions = await loadDefinitions(required(recordId, 'definition file'));
75
+ const results = [];
76
+ for (const definition of definitions)
77
+ results.push(await toolbox.apply(definition));
78
+ write(stdout, results.length === 1 ? results[0] : results);
79
+ break;
80
+ }
81
+ case 'list':
82
+ write(stdout, toolbox.list(parsed.loadout));
83
+ break;
84
+ case 'catalog':
85
+ write(stdout, toolbox.catalog());
86
+ break;
87
+ case 'show': {
88
+ const value = required(recordId, 'tool or action id');
89
+ const result = toolbox.get(value) || toolbox.action(value);
90
+ if (!result)
91
+ throw new Error(`Unknown tool or action: ${value}`);
92
+ write(stdout, result);
93
+ break;
94
+ }
95
+ case 'enable':
96
+ write(stdout, await toolbox.setStatus(required(recordId, 'record id'), 'enabled'));
97
+ break;
98
+ case 'disable':
99
+ write(stdout, await toolbox.setStatus(required(recordId, 'record id'), 'disabled'));
100
+ break;
101
+ case 'remove':
102
+ write(stdout, await toolbox.remove(required(recordId, 'record id')));
103
+ break;
104
+ case 'run':
105
+ write(stdout, await toolbox.call(required(recordId, 'action id'), parsed.input, { loadout: parsed.loadout }));
106
+ break;
107
+ case 'connections':
108
+ write(stdout, toolbox.connections(parsed.loadout));
109
+ break;
110
+ default: throw new Error(`Unknown command: ${parsed.command}`);
111
+ }
112
+ return 0;
113
+ }
114
+ catch (error) {
115
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
116
+ return 1;
117
+ }
118
+ finally {
119
+ if (!io.toolbox)
120
+ toolbox.close();
121
+ }
122
+ }
123
+ export { HELP, parse, runCli };
@@ -0,0 +1,7 @@
1
+ import type { ActionRecord, ToolDefinition, ToolRecord } from './types.js';
2
+ declare function normalizeDefinition(definition: ToolDefinition, existing?: ToolRecord, now?: string): {
3
+ tool: ToolRecord;
4
+ actions: ActionRecord[];
5
+ };
6
+ declare function defineTool(definition: ToolDefinition): ToolDefinition;
7
+ export { defineTool, normalizeDefinition };
@@ -0,0 +1,170 @@
1
+ import { actionId, actionName, id } from './ids.js';
2
+ const DEFAULT_TIMEOUT = 30_000;
3
+ const DEFAULT_MAX_OUTPUT = 256_000;
4
+ function string(value, label) {
5
+ const result = typeof value === 'string' ? value.trim() : '';
6
+ if (!result)
7
+ throw new Error(`${label} is required`);
8
+ return result;
9
+ }
10
+ function strings(value, label) {
11
+ if (value === undefined)
12
+ return undefined;
13
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
14
+ throw new Error(`${label} must be an array of strings`);
15
+ }
16
+ return [...value];
17
+ }
18
+ function positive(value, fallback, label) {
19
+ if (value === undefined)
20
+ return fallback;
21
+ if (!Number.isSafeInteger(value) || Number(value) <= 0)
22
+ throw new Error(`${label} must be a positive integer`);
23
+ return Number(value);
24
+ }
25
+ function refs(value, label) {
26
+ if (value === undefined)
27
+ return undefined;
28
+ if (!value || typeof value !== 'object' || Array.isArray(value))
29
+ throw new Error(`${label} must be an object`);
30
+ const result = {};
31
+ for (const [key, reference] of Object.entries(value))
32
+ result[string(key, label)] = string(reference, `${label}.${key}`);
33
+ return result;
34
+ }
35
+ function base(source) {
36
+ return {
37
+ timeoutMs: positive(source.timeoutMs, DEFAULT_TIMEOUT, 'source.timeoutMs'),
38
+ maxOutputBytes: positive(source.maxOutputBytes, DEFAULT_MAX_OUTPUT, 'source.maxOutputBytes'),
39
+ };
40
+ }
41
+ function normalizeSource(source) {
42
+ if (!source || typeof source !== 'object')
43
+ throw new Error('tool.source is required');
44
+ if ('headers' in source || 'env' in source) {
45
+ throw new Error('Inline headers and environment values are forbidden; use secret references');
46
+ }
47
+ if (source.type === 'cli') {
48
+ const inputMode = source.inputMode || 'json-stdin';
49
+ const outputMode = source.outputMode || 'text';
50
+ if (!['json-stdin', 'argv', 'none'].includes(inputMode))
51
+ throw new Error(`Unsupported CLI input mode: ${inputMode}`);
52
+ if (!['json', 'text'].includes(outputMode))
53
+ throw new Error(`Unsupported CLI output mode: ${outputMode}`);
54
+ return {
55
+ type: 'cli', command: string(source.command, 'source.command'),
56
+ ...base(source), inputMode, outputMode,
57
+ ...(strings(source.args, 'source.args') ? { args: strings(source.args, 'source.args') } : {}),
58
+ ...(source.cwd ? { cwd: string(source.cwd, 'source.cwd') } : {}),
59
+ ...(refs(source.secretEnv, 'source.secretEnv') ? { secretEnv: refs(source.secretEnv, 'source.secretEnv') } : {}),
60
+ };
61
+ }
62
+ if (source.type === 'api') {
63
+ const url = new URL(string(source.baseUrl, 'source.baseUrl'));
64
+ if (!['http:', 'https:'].includes(url.protocol))
65
+ throw new Error('source.baseUrl must use http or https');
66
+ if (url.username || url.password)
67
+ throw new Error('source.baseUrl cannot contain credentials');
68
+ return {
69
+ type: 'api', baseUrl: url.toString(), ...base(source),
70
+ ...(refs(source.secretHeaders, 'source.secretHeaders') ? { secretHeaders: refs(source.secretHeaders, 'source.secretHeaders') } : {}),
71
+ };
72
+ }
73
+ if (source.type === 'mcp') {
74
+ if (!['http', 'sse', 'stdio', 'host'].includes(source.transport))
75
+ throw new Error(`Unsupported MCP transport: ${source.transport}`);
76
+ if (['http', 'sse'].includes(source.transport) && !source.url)
77
+ throw new Error(`${source.transport} MCP tools require source.url`);
78
+ if (source.transport === 'stdio' && !source.command)
79
+ throw new Error('stdio MCP tools require source.command');
80
+ if (source.url) {
81
+ const url = new URL(source.url);
82
+ if (!['http:', 'https:'].includes(url.protocol))
83
+ throw new Error('source.url must use http or https');
84
+ if (url.username || url.password)
85
+ throw new Error('source.url cannot contain credentials');
86
+ }
87
+ return {
88
+ type: 'mcp', transport: source.transport, ...base(source),
89
+ ...(source.url ? { url: string(source.url, 'source.url') } : {}),
90
+ ...(source.command ? { command: string(source.command, 'source.command') } : {}),
91
+ ...(strings(source.args, 'source.args') ? { args: strings(source.args, 'source.args') } : {}),
92
+ ...(source.cwd ? { cwd: string(source.cwd, 'source.cwd') } : {}),
93
+ ...(source.serverName ? { serverName: string(source.serverName, 'source.serverName') } : {}),
94
+ ...(refs(source.secretHeaders, 'source.secretHeaders') ? { secretHeaders: refs(source.secretHeaders, 'source.secretHeaders') } : {}),
95
+ ...(refs(source.secretEnv, 'source.secretEnv') ? { secretEnv: refs(source.secretEnv, 'source.secretEnv') } : {}),
96
+ };
97
+ }
98
+ throw new Error(`Unsupported tool type: ${source.type}`);
99
+ }
100
+ function inputSchema(value) {
101
+ if (value === undefined)
102
+ return { type: 'object', properties: {}, additionalProperties: true };
103
+ if (!value || typeof value !== 'object' || Array.isArray(value) || value.type !== 'object') {
104
+ throw new Error('action.inputSchema must be an object schema');
105
+ }
106
+ const schema = structuredClone(value);
107
+ if (schema.properties !== undefined && (!schema.properties || typeof schema.properties !== 'object' || Array.isArray(schema.properties))) {
108
+ throw new Error('action.inputSchema.properties must be an object');
109
+ }
110
+ return { ...schema, properties: schema.properties || {} };
111
+ }
112
+ function target(type, value) {
113
+ const raw = value || {};
114
+ if (type === 'cli')
115
+ return { ...(strings(raw.args, 'action.target.args') ? { args: strings(raw.args, 'action.target.args') } : {}) };
116
+ if (type === 'api')
117
+ return { method: string(raw.method, 'action.target.method').toUpperCase(), path: string(raw.path, 'action.target.path') };
118
+ return { name: string(raw.name, 'action.target.name') };
119
+ }
120
+ function normalizeDefinition(definition, existing, now = new Date().toISOString()) {
121
+ if (!definition || typeof definition !== 'object')
122
+ throw new Error('tool definition must be an object');
123
+ const toolId = id(definition.id, 'tool.id', 64);
124
+ const source = normalizeSource(definition.source);
125
+ const origin = definition.origin || 'user';
126
+ if (!['user', 'catalog', 'system'].includes(origin))
127
+ throw new Error(`Unsupported tool origin: ${origin}`);
128
+ const tool = {
129
+ id: toolId, name: string(definition.name, 'tool.name'), source,
130
+ owner: definition.owner?.trim() || 'user', origin,
131
+ status: definition.status || 'enabled',
132
+ ...(definition.description?.trim() ? { description: definition.description.trim() } : {}),
133
+ ...(definition.display ? { display: structuredClone(definition.display) } : {}),
134
+ ...(definition.policy ? { policy: structuredClone(definition.policy) } : {}),
135
+ ...(definition.metadata ? { metadata: structuredClone(definition.metadata) } : {}),
136
+ createdAt: existing?.createdAt || now, updatedAt: now,
137
+ };
138
+ if (!['enabled', 'disabled'].includes(tool.status))
139
+ throw new Error(`Unsupported tool status: ${tool.status}`);
140
+ const definitions = definition.actions || (source.type === 'cli' ? [{ name: 'run' }] : []);
141
+ const seen = new Set();
142
+ const actions = definitions.map((action) => {
143
+ const name = actionName(action.name);
144
+ const canonicalId = actionId(toolId, name);
145
+ if (action.id && id(action.id, 'action.id') !== canonicalId)
146
+ throw new Error(`Action id must be ${canonicalId}`);
147
+ if (seen.has(canonicalId))
148
+ throw new Error(`Duplicate action: ${canonicalId}`);
149
+ seen.add(canonicalId);
150
+ const status = action.status || 'enabled';
151
+ if (!['enabled', 'disabled'].includes(status))
152
+ throw new Error(`Unsupported action status: ${status}`);
153
+ return {
154
+ id: canonicalId, toolId, name, status,
155
+ inputSchema: inputSchema(action.inputSchema), target: target(source.type, action.target),
156
+ ...(action.displayName?.trim() ? { displayName: action.displayName.trim() } : {}),
157
+ ...(action.description?.trim() ? { description: action.description.trim() } : {}),
158
+ ...(action.outputSchema ? { outputSchema: structuredClone(action.outputSchema) } : {}),
159
+ ...(action.policy ? { policy: structuredClone(action.policy) } : {}),
160
+ ...(action.metadata ? { metadata: structuredClone(action.metadata) } : {}),
161
+ createdAt: now, updatedAt: now,
162
+ };
163
+ });
164
+ return { tool, actions };
165
+ }
166
+ function defineTool(definition) {
167
+ normalizeDefinition(definition);
168
+ return structuredClone(definition);
169
+ }
170
+ export { defineTool, normalizeDefinition };
package/dist/ids.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { ActionRecord } from './types.js';
2
+ declare function id(value: unknown, label?: string, maximumLength?: number): string;
3
+ declare function actionName(value: unknown): string;
4
+ declare function actionId(toolId: string, name: string): string;
5
+ declare function mcpName(action: Pick<ActionRecord, 'id'>): string;
6
+ declare function assertMcpNamesUnique(actions: Pick<ActionRecord, 'id'>[]): void;
7
+ export { actionId, actionName, assertMcpNamesUnique, id, mcpName };
package/dist/ids.js ADDED
@@ -0,0 +1,38 @@
1
+ function id(value, label = 'id', maximumLength = 120) {
2
+ const result = String(value || '').trim().toLowerCase();
3
+ if (!result)
4
+ throw new Error(`${label} is required`);
5
+ if (result.length > maximumLength)
6
+ throw new Error(`${label} must be at most ${maximumLength} characters`);
7
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(result)) {
8
+ throw new Error(`${label} must contain only lowercase letters, numbers, dots, underscores, and hyphens`);
9
+ }
10
+ return result;
11
+ }
12
+ function actionName(value) {
13
+ const result = id(value, 'action.name', 48);
14
+ if (result.includes('.'))
15
+ throw new Error('action.name cannot contain dots');
16
+ return result;
17
+ }
18
+ function actionId(toolId, name) {
19
+ return `${id(toolId, 'tool.id', 64)}.${actionName(name)}`;
20
+ }
21
+ function safeMcpName(value) {
22
+ return value.replace(/[^A-Za-z0-9_-]+/g, '_').replace(/_+/g, '_');
23
+ }
24
+ function mcpName(action) {
25
+ return `toolbox__${safeMcpName(action.id)}`;
26
+ }
27
+ function assertMcpNamesUnique(actions) {
28
+ const names = new Map();
29
+ for (const action of actions) {
30
+ const name = mcpName(action);
31
+ const existing = names.get(name);
32
+ if (existing && existing !== action.id) {
33
+ throw new Error(`Actions ${existing} and ${action.id} collide as MCP tool ${name}`);
34
+ }
35
+ names.set(name, action.id);
36
+ }
37
+ }
38
+ export { actionId, actionName, assertMcpNamesUnique, id, mcpName };
@@ -0,0 +1,9 @@
1
+ export { ArtifactFiles } from './artifact-files.js';
2
+ export { LEGACY_TOOLBOX_BACKUP_FILE_NAME, LEGACY_TOOLBOX_FILE_NAME, TOOL_ARTIFACT_KIND, TOOL_ARTIFACT_SCHEMA_VERSION, TOOLBOX_INDEX_FILE_NAME, artifactDocument, catalogIndexDocument, isToolArtifactDocument, legacyMigrationPlan, safeToolId, toolboxIndexDocument, userArtifactFileName, } from './artifacts.js';
3
+ export type { ArtifactFileWrite, LegacyMigrationPlan, ToolArtifactDocument, ToolboxIndexDocument, } from './artifacts.js';
4
+ export { defineTool, normalizeDefinition } from './definition.js';
5
+ export { actionId, actionName, id, mcpName } from './ids.js';
6
+ export { actionTools, callMcpTool, createMcpTools, findMcpTool, managementTools, } from './mcp.js';
7
+ export { createMcpServer } from './mcp-server.js';
8
+ export { Toolbox } from './toolbox.js';
9
+ export type * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { ArtifactFiles } from './artifact-files.js';
2
+ export { LEGACY_TOOLBOX_BACKUP_FILE_NAME, LEGACY_TOOLBOX_FILE_NAME, TOOL_ARTIFACT_KIND, TOOL_ARTIFACT_SCHEMA_VERSION, TOOLBOX_INDEX_FILE_NAME, artifactDocument, catalogIndexDocument, isToolArtifactDocument, legacyMigrationPlan, safeToolId, toolboxIndexDocument, userArtifactFileName, } from './artifacts.js';
3
+ export { defineTool, normalizeDefinition } from './definition.js';
4
+ export { actionId, actionName, id, mcpName } from './ids.js';
5
+ export { actionTools, callMcpTool, createMcpTools, findMcpTool, managementTools, } from './mcp.js';
6
+ export { createMcpServer } from './mcp-server.js';
7
+ export { Toolbox } from './toolbox.js';
@@ -0,0 +1,4 @@
1
+ import type { JsonSchema } from './types.js';
2
+ declare function objectInput(value: unknown): Record<string, unknown>;
3
+ declare function validateInput(schema: JsonSchema, value: unknown): Record<string, unknown>;
4
+ export { objectInput, validateInput };
package/dist/input.js ADDED
@@ -0,0 +1,43 @@
1
+ function objectInput(value) {
2
+ if (value === undefined)
3
+ return {};
4
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
5
+ throw new Error('Tool input must be an object');
6
+ }
7
+ return value;
8
+ }
9
+ function matchesType(value, type) {
10
+ if (type === 'array')
11
+ return Array.isArray(value);
12
+ if (type === 'integer')
13
+ return Number.isInteger(value);
14
+ if (type === 'number')
15
+ return typeof value === 'number' && Number.isFinite(value);
16
+ if (type === 'object')
17
+ return !!value && typeof value === 'object' && !Array.isArray(value);
18
+ if (type === 'null')
19
+ return value === null;
20
+ return typeof value === type;
21
+ }
22
+ function validateInput(schema, value) {
23
+ const input = objectInput(value);
24
+ for (const key of schema.required || []) {
25
+ if (!(key in input))
26
+ throw new Error(`Tool input is missing required property: ${key}`);
27
+ }
28
+ if (schema.additionalProperties === false) {
29
+ const known = new Set(Object.keys(schema.properties || {}));
30
+ const extra = Object.keys(input).find((key) => !known.has(key));
31
+ if (extra)
32
+ throw new Error(`Tool input contains unknown property: ${extra}`);
33
+ }
34
+ for (const [key, property] of Object.entries(schema.properties || {})) {
35
+ if (!(key in input) || !property.type)
36
+ continue;
37
+ if (!matchesType(input[key], property.type)) {
38
+ throw new Error(`Tool input property ${key} must be ${property.type}`);
39
+ }
40
+ }
41
+ return input;
42
+ }
43
+ export { objectInput, validateInput };
@@ -0,0 +1,18 @@
1
+ import readline from 'node:readline';
2
+ import type { Readable, Writable } from 'node:stream';
3
+ import { Toolbox } from './toolbox.js';
4
+ import type { ToolboxOptions } from './types.js';
5
+ interface Request {
6
+ jsonrpc?: string;
7
+ id?: string | number;
8
+ method: string;
9
+ params?: Record<string, any>;
10
+ }
11
+ declare function createMcpServer(options?: ToolboxOptions & {
12
+ toolbox?: Toolbox;
13
+ }): {
14
+ toolbox: Toolbox;
15
+ handle: (request: Request) => Promise<unknown>;
16
+ start: (input?: Readable, output?: Writable) => readline.Interface;
17
+ };
18
+ export { createMcpServer };
@@ -0,0 +1,61 @@
1
+ import readline from 'node:readline';
2
+ import { callMcpTool, createMcpTools } from './mcp.js';
3
+ import { Toolbox } from './toolbox.js';
4
+ function descriptor(tool) {
5
+ const { handler, ...definition } = tool;
6
+ return definition;
7
+ }
8
+ function createMcpServer(options = {}) {
9
+ const toolbox = options.toolbox || new Toolbox(options);
10
+ async function handle(request) {
11
+ if (request.method === 'initialize') {
12
+ return {
13
+ protocolVersion: request.params?.protocolVersion || '2024-11-05',
14
+ capabilities: { tools: { listChanged: true } },
15
+ serverInfo: { name: 'amalgm-tools', version: '0.1.0' },
16
+ };
17
+ }
18
+ if (request.method === 'ping')
19
+ return {};
20
+ const tools = createMcpTools(toolbox);
21
+ if (request.method === 'tools/list')
22
+ return { tools: tools.map(descriptor) };
23
+ if (request.method === 'tools/call') {
24
+ try {
25
+ return await callMcpTool(toolbox, String(request.params?.name || ''), request.params?.arguments || {});
26
+ }
27
+ catch (error) {
28
+ if (error instanceof Error && error.message.startsWith('Unknown tool:')) {
29
+ Object.assign(error, { code: -32602 });
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+ if (request.method.startsWith('notifications/'))
35
+ return undefined;
36
+ throw Object.assign(new Error(`Method not found: ${request.method}`), { code: -32601 });
37
+ }
38
+ function start(input = process.stdin, output = process.stdout) {
39
+ const lines = readline.createInterface({ input, crlfDelay: Infinity });
40
+ lines.on('line', async (line) => {
41
+ let request;
42
+ try {
43
+ request = JSON.parse(line);
44
+ const result = await handle(request);
45
+ if (request.id !== undefined && result !== undefined)
46
+ output.write(`${JSON.stringify({ jsonrpc: '2.0', id: request.id, result })}\n`);
47
+ }
48
+ catch (error) {
49
+ if (request?.id === undefined)
50
+ return;
51
+ output.write(`${JSON.stringify({
52
+ jsonrpc: '2.0', id: request.id,
53
+ error: { code: error?.code || -32603, message: error instanceof Error ? error.message : String(error) },
54
+ })}\n`);
55
+ }
56
+ });
57
+ return lines;
58
+ }
59
+ return { toolbox, handle, start };
60
+ }
61
+ export { createMcpServer };
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Toolbox } from './toolbox.js';
2
+ import type { JsonSchema, McpOptions, McpTool } from './types.js';
3
+ declare function portable(schema: JsonSchema): JsonSchema;
4
+ declare function managementTools(toolbox: Toolbox): McpTool[];
5
+ declare function actionTools(toolbox: Toolbox, options?: McpOptions): McpTool[];
6
+ declare function createMcpTools(toolbox: Toolbox, options?: McpOptions): McpTool[];
7
+ declare function findMcpTool(toolbox: Toolbox, name: string, options?: McpOptions): McpTool | null;
8
+ declare function callMcpTool(toolbox: Toolbox, name: string, input?: Record<string, unknown>, options?: McpOptions): Promise<import("./types.js").ToolResult>;
9
+ export { actionTools, callMcpTool, createMcpTools, findMcpTool, managementTools, portable, };
package/dist/mcp.js ADDED
@@ -0,0 +1,95 @@
1
+ import { mcpName } from './ids.js';
2
+ import { errorResult, structuredResult } from './results.js';
3
+ function portable(schema) {
4
+ const result = structuredClone(schema);
5
+ for (const key of ['anyOf', 'oneOf', 'allOf', 'not', 'enum'])
6
+ delete result[key];
7
+ result.type = 'object';
8
+ result.properties ||= {};
9
+ return result;
10
+ }
11
+ function managed(definition, handler) {
12
+ return {
13
+ ...definition,
14
+ async handler(input = {}) {
15
+ try {
16
+ return structuredResult(await handler(input));
17
+ }
18
+ catch (error) {
19
+ return errorResult(error);
20
+ }
21
+ },
22
+ };
23
+ }
24
+ function managementTools(toolbox) {
25
+ const idProperty = { id: { type: 'string', description: 'Tool or action id' } };
26
+ return [
27
+ managed({
28
+ name: 'toolbox_tools_list', description: 'List the canonical Toolbox catalog.',
29
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
30
+ }, () => toolbox.catalog()),
31
+ managed({
32
+ name: 'toolbox_tool_get', description: 'Get one tool and its actions.',
33
+ inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
34
+ }, ({ id }) => {
35
+ const result = toolbox.get(String(id));
36
+ if (!result)
37
+ throw new Error(`Unknown tool: ${id}`);
38
+ return result;
39
+ }),
40
+ managed({
41
+ name: 'toolbox_tool_apply', description: 'Atomically apply an authoritative tool definition.',
42
+ inputSchema: {
43
+ type: 'object', required: ['tool'], additionalProperties: false,
44
+ properties: { tool: { type: 'object', description: 'Complete tool definition' } },
45
+ },
46
+ }, ({ tool }) => toolbox.apply(tool)),
47
+ managed({
48
+ name: 'toolbox_record_enable', description: 'Enable one user-owned tool or action.',
49
+ inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
50
+ }, ({ id }) => toolbox.setStatus(String(id), 'enabled')),
51
+ managed({
52
+ name: 'toolbox_record_disable', description: 'Disable one user-owned tool or action.',
53
+ inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
54
+ }, ({ id }) => toolbox.setStatus(String(id), 'disabled')),
55
+ managed({
56
+ name: 'toolbox_record_remove', description: 'Remove one user-owned tool or action.',
57
+ inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
58
+ }, ({ id }) => toolbox.remove(String(id))),
59
+ managed({
60
+ name: 'toolbox_mcp_connections', description: 'List external MCP connections for the composing host.',
61
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
62
+ }, () => toolbox.connections()),
63
+ ];
64
+ }
65
+ function actionTools(toolbox, options = {}) {
66
+ return toolbox.callable(options.loadout).map(({ tool, action }) => ({
67
+ name: mcpName(action),
68
+ description: action.description || `Run ${tool.name}: ${action.displayName || action.name}.`,
69
+ inputSchema: portable(action.inputSchema),
70
+ async handler(input = {}) {
71
+ try {
72
+ return await toolbox.call(action.id, input, options);
73
+ }
74
+ catch (error) {
75
+ return errorResult(error);
76
+ }
77
+ },
78
+ }));
79
+ }
80
+ function createMcpTools(toolbox, options = {}) {
81
+ return [
82
+ ...(options.includeManagement === false ? [] : managementTools(toolbox)),
83
+ ...actionTools(toolbox, options),
84
+ ];
85
+ }
86
+ function findMcpTool(toolbox, name, options = {}) {
87
+ return createMcpTools(toolbox, options).find((tool) => tool.name === name) || null;
88
+ }
89
+ async function callMcpTool(toolbox, name, input = {}, options = {}) {
90
+ const tool = findMcpTool(toolbox, name, options);
91
+ if (!tool)
92
+ throw new Error(`Unknown tool: ${name}`);
93
+ return tool.handler(input);
94
+ }
95
+ export { actionTools, callMcpTool, createMcpTools, findMcpTool, managementTools, portable, };
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition } from './types.js';
2
+ declare function loadDefinitions(file: string): Promise<ToolDefinition[]>;
3
+ export { loadDefinitions };
package/dist/module.js ADDED
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ async function loadDefinitions(file) {
5
+ const absolute = path.resolve(file);
6
+ if (!fs.existsSync(absolute))
7
+ throw new Error(`Definition file not found: ${absolute}`);
8
+ let value;
9
+ if (path.extname(absolute).toLowerCase() === '.json') {
10
+ value = JSON.parse(fs.readFileSync(absolute, 'utf8'));
11
+ }
12
+ else {
13
+ const loaded = await import(`${pathToFileURL(absolute).href}?updated=${fs.statSync(absolute).mtimeMs}`);
14
+ value = loaded.default ?? loaded.tools ?? loaded.tool;
15
+ }
16
+ const definitions = Array.isArray(value) ? value : [value];
17
+ if (!definitions.length || definitions.some((definition) => !definition || typeof definition !== 'object')) {
18
+ throw new Error('Definition module must export one tool or an array of tools');
19
+ }
20
+ return definitions;
21
+ }
22
+ export { loadDefinitions };
@@ -0,0 +1,22 @@
1
+ interface CommandInput {
2
+ command: string;
3
+ args: string[];
4
+ cwd?: string;
5
+ env: NodeJS.ProcessEnv;
6
+ stdin?: string;
7
+ timeoutMs: number;
8
+ maximumBytes: number;
9
+ signal?: AbortSignal;
10
+ }
11
+ interface CommandOutput {
12
+ code: number | null;
13
+ signal: NodeJS.Signals | null;
14
+ stdout: string;
15
+ stderr: string;
16
+ timedOut: boolean;
17
+ aborted: boolean;
18
+ truncated: boolean;
19
+ }
20
+ declare function runCommand(input: CommandInput): Promise<CommandOutput>;
21
+ export { runCommand };
22
+ export type { CommandInput, CommandOutput };