@amalgm/tools 0.1.7-preview.34436591947 → 0.1.7-preview.34547098528

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 CHANGED
@@ -66,9 +66,15 @@ amalgm-tools apply ./tool.mjs
66
66
  amalgm-tools list
67
67
  amalgm-tools run hello.say --input '{"name":"Ada"}'
68
68
  amalgm-tools disable hello.say
69
+ amalgm-tools link canvas app-3f1c
69
70
  amalgm-tools connections
70
71
  ```
71
72
 
73
+ A tool may belong to an app: `appId` on the tool is the reverse edge of the
74
+ app manifest's `toolIds`, and sharing either side carries the other. The host
75
+ that composes Tools and Apps may pass `environment(tool)` so a linked tool
76
+ runs knowing where its app listens.
77
+
72
78
  `amalgm-tools-mcp` serves Toolbox management and enabled action tools over
73
79
  newline-delimited MCP stdio.
74
80
 
@@ -1,3 +1,4 @@
1
- import type { ToolDriver } from './types.js';
2
- declare const cliDriver: ToolDriver;
3
- export { cliDriver };
1
+ import type { ToolDriver, ToolRecord } from './types.js';
2
+ type Environment = (tool: ToolRecord) => Record<string, string> | Promise<Record<string, string>>;
3
+ declare function createCliDriver(environment?: Environment): ToolDriver;
4
+ export { createCliDriver };
@@ -8,45 +8,47 @@ function inputArguments(input) {
8
8
  }
9
9
  return args;
10
10
  }
11
- const cliDriver = {
12
- type: 'cli',
13
- async call({ tool, action, input, options }) {
14
- const source = tool.source;
15
- const target = action.target;
16
- const environment = await resolveReferences(source.secretEnv, options.resolveSecret);
17
- const args = [...(source.args || []), ...(target.args || [])];
18
- if (source.inputMode === 'argv')
19
- args.push(...inputArguments(input));
20
- const output = await runCommand({
21
- command: source.command, args, cwd: source.cwd,
22
- env: { ...process.env, ...environment },
23
- stdin: source.inputMode === 'json-stdin' ? `${JSON.stringify(input)}\n` : undefined,
24
- timeoutMs: source.timeoutMs, maximumBytes: source.maxOutputBytes, signal: options.signal,
25
- });
26
- const stdout = output.stdout.trim();
27
- const stderr = output.stderr.trim();
28
- if (output.timedOut)
29
- return errorResult(`Tool ${action.id} timed out after ${source.timeoutMs}ms`);
30
- if (output.aborted)
31
- return errorResult(`Tool ${action.id} was cancelled`);
32
- if (output.code !== 0) {
33
- return errorResult([
34
- `Tool ${action.id} exited with ${output.code ?? output.signal ?? 'unknown'}.`,
35
- stdout && `stdout:\n${stdout}`, stderr && `stderr:\n${stderr}`,
36
- ].filter(Boolean).join('\n\n'));
37
- }
38
- let result;
39
- if (source.outputMode === 'json' && stdout) {
40
- try {
41
- result = structuredResult(JSON.parse(stdout));
11
+ function createCliDriver(environment = () => ({})) {
12
+ return {
13
+ type: 'cli',
14
+ async call({ tool, action, input, options }) {
15
+ const source = tool.source;
16
+ const target = action.target;
17
+ const secrets = await resolveReferences(source.secretEnv, options.resolveSecret);
18
+ const args = [...(source.args || []), ...(target.args || [])];
19
+ if (source.inputMode === 'argv')
20
+ args.push(...inputArguments(input));
21
+ const output = await runCommand({
22
+ command: source.command, args, cwd: source.cwd,
23
+ env: { ...process.env, ...(await environment(tool)), ...secrets },
24
+ stdin: source.inputMode === 'json-stdin' ? `${JSON.stringify(input)}\n` : undefined,
25
+ timeoutMs: source.timeoutMs, maximumBytes: source.maxOutputBytes, signal: options.signal,
26
+ });
27
+ const stdout = output.stdout.trim();
28
+ const stderr = output.stderr.trim();
29
+ if (output.timedOut)
30
+ return errorResult(`Tool ${action.id} timed out after ${source.timeoutMs}ms`);
31
+ if (output.aborted)
32
+ return errorResult(`Tool ${action.id} was cancelled`);
33
+ if (output.code !== 0) {
34
+ return errorResult([
35
+ `Tool ${action.id} exited with ${output.code ?? output.signal ?? 'unknown'}.`,
36
+ stdout && `stdout:\n${stdout}`, stderr && `stderr:\n${stderr}`,
37
+ ].filter(Boolean).join('\n\n'));
42
38
  }
43
- catch {
44
- return errorResult(`Tool ${action.id} returned invalid JSON`);
39
+ let result;
40
+ if (source.outputMode === 'json' && stdout) {
41
+ try {
42
+ result = structuredResult(JSON.parse(stdout));
43
+ }
44
+ catch {
45
+ return errorResult(`Tool ${action.id} returned invalid JSON`);
46
+ }
45
47
  }
46
- }
47
- else
48
- result = textResult(stdout || `Tool ${action.id} completed.`);
49
- return { ...result, ...(output.truncated ? { truncated: true } : {}) };
50
- },
51
- };
52
- export { cliDriver };
48
+ else
49
+ result = textResult(stdout || `Tool ${action.id} completed.`);
50
+ return { ...result, ...(output.truncated ? { truncated: true } : {}) };
51
+ },
52
+ };
53
+ }
54
+ export { createCliDriver };
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CliIo, LoadoutInput } from './types.js';
2
- declare const HELP = "Usage: amalgm-tools [--state-dir DIR] [--loadout IDS] <command>\n\nCommands:\n apply <definition.js|json> Apply one tool or an array of tools\n list List tools and enabled actions\n catalog Print the complete canonical catalog\n show <tool-or-action-id> Show one record\n enable <tool-or-action-id> Enable one record\n disable <tool-or-action-id> Disable one record\n remove <tool-or-action-id> Remove one record\n run <action-id> [--input JSON]\n Run an action\n connections List MCP connections for a composing host";
2
+ declare const HELP = "Usage: amalgm-tools [--state-dir DIR] [--loadout IDS] <command>\n\nCommands:\n apply <definition.js|json> Apply one tool or an array of tools\n list List tools and enabled actions\n catalog Print the complete canonical catalog\n show <tool-or-action-id> Show one record\n enable <tool-or-action-id> Enable one record\n disable <tool-or-action-id> Disable one record\n remove <tool-or-action-id> Remove one record\n link <tool-id> <app-id> Link one tool to the app it belongs to\n unlink <tool-id> Detach one tool from its app\n run <action-id> [--input JSON]\n Run an action\n connections List MCP connections for a composing host";
3
3
  interface Parsed {
4
4
  command?: string;
5
5
  args: string[];
@@ -7,7 +7,12 @@ interface Parsed {
7
7
  loadout?: LoadoutInput;
8
8
  input: Record<string, unknown>;
9
9
  }
10
+ /** One call on the command surface: an MCP tool name and its arguments. */
11
+ interface Call {
12
+ name: string;
13
+ args: Record<string, unknown>;
14
+ }
10
15
  declare function parse(argv: string[]): Parsed;
11
16
  declare function runCli(argv: string[], io?: CliIo): Promise<number>;
12
17
  export { HELP, parse, runCli };
13
- export type { Parsed };
18
+ export type { Call, Parsed };
package/dist/cli.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { createToolboxCommands } from './commands.js';
2
+ import { id, mcpName } from './ids.js';
1
3
  import { loadDefinitions } from './module.js';
2
4
  import { Toolbox } from './toolbox.js';
3
5
  const HELP = `Usage: amalgm-tools [--state-dir DIR] [--loadout IDS] <command>
@@ -10,6 +12,8 @@ Commands:
10
12
  enable <tool-or-action-id> Enable one record
11
13
  disable <tool-or-action-id> Disable one record
12
14
  remove <tool-or-action-id> Remove one record
15
+ link <tool-id> <app-id> Link one tool to the app it belongs to
16
+ unlink <tool-id> Detach one tool from its app
13
17
  run <action-id> [--input JSON]
14
18
  Run an action
15
19
  connections List MCP connections for a composing host`;
@@ -48,9 +52,44 @@ function required(value, label) {
48
52
  throw new Error(`${label} is required`);
49
53
  return value;
50
54
  }
55
+ /** Map one parsed command line to its calls; `apply` makes one call per definition. */
56
+ async function calls(parsed) {
57
+ const [first, second] = parsed.args;
58
+ const record = (name, label) => ({ name, args: { id: required(first, label) } });
59
+ switch (parsed.command) {
60
+ case 'apply': {
61
+ const definitions = await loadDefinitions(required(first, 'definition file'));
62
+ return definitions.map((tool) => ({ name: 'toolbox_tool_apply', args: { tool } }));
63
+ }
64
+ case 'list': return [{ name: 'toolbox_tools_selected', args: {} }];
65
+ case 'catalog': return [{ name: 'toolbox_tools_list', args: {} }];
66
+ case 'show': return [record('toolbox_tool_get', 'tool or action id')];
67
+ case 'enable': return [record('toolbox_record_enable', 'record id')];
68
+ case 'disable': return [record('toolbox_record_disable', 'record id')];
69
+ case 'remove': return [record('toolbox_record_remove', 'record id')];
70
+ case 'link': return [{
71
+ name: 'toolbox_tool_link',
72
+ args: { id: required(first, 'tool id'), app_id: required(second, 'app id') },
73
+ }];
74
+ case 'unlink': return [record('toolbox_tool_unlink', 'tool id')];
75
+ case 'run': return [{ name: mcpName({ id: id(first, 'action id') }), args: parsed.input }];
76
+ case 'connections': return [{ name: 'toolbox_mcp_connections', args: {} }];
77
+ default: throw new Error(`Unknown command: ${parsed.command}`);
78
+ }
79
+ }
80
+ function open(io, parsed) {
81
+ if (io.commands)
82
+ return { commands: io.commands, close() { } };
83
+ const toolbox = new Toolbox({ stateDir: parsed.stateDir });
84
+ return { commands: createToolboxCommands(toolbox, parsed.loadout), close: () => toolbox.close() };
85
+ }
51
86
  function write(output, value) {
52
87
  output.write(`${JSON.stringify(value, null, 2)}\n`);
53
88
  }
89
+ function fail(output, error) {
90
+ output.write(`${error instanceof Error ? error.message : String(error)}\n`);
91
+ return 1;
92
+ }
54
93
  async function runCli(argv, io = {}) {
55
94
  const stdout = io.stdout || process.stdout;
56
95
  const stderr = io.stderr || process.stderr;
@@ -59,65 +98,28 @@ async function runCli(argv, io = {}) {
59
98
  parsed = parse(argv);
60
99
  }
61
100
  catch (error) {
62
- stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
63
- return 1;
101
+ return fail(stderr, error);
64
102
  }
65
103
  if (!parsed.command || ['help', '-h', '--help'].includes(parsed.command)) {
66
104
  stdout.write(`${HELP}\n`);
67
105
  return 0;
68
106
  }
69
- const toolbox = io.toolbox || new Toolbox({ stateDir: parsed.stateDir });
70
107
  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}`);
108
+ const planned = await calls(parsed);
109
+ const session = open(io, parsed);
110
+ try {
111
+ const results = [];
112
+ for (const call of planned)
113
+ results.push(await session.commands.execute(call.name, call.args));
114
+ write(stdout, results.length === 1 ? results[0] : results);
115
+ }
116
+ finally {
117
+ session.close();
111
118
  }
112
119
  return 0;
113
120
  }
114
121
  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();
122
+ return fail(stderr, error);
121
123
  }
122
124
  }
123
125
  export { HELP, parse, runCli };
@@ -0,0 +1,27 @@
1
+ import type { Toolbox } from './toolbox.js';
2
+ import type { LoadoutInput } from './types.js';
3
+ /**
4
+ * The one command surface. The CLI speaks only this; a host forwards it to a
5
+ * running runtime's MCP over HTTP, and `createToolboxCommands` runs it against
6
+ * a local Toolbox through that Toolbox's own MCP tools.
7
+ *
8
+ * `name` is an MCP tool name from this package's MCP surface: a management
9
+ * tool (`toolbox_*`, single underscore) or an action tool (`toolbox__<id>`,
10
+ * from `mcpName`). `args` is the MCP `arguments` object for that tool.
11
+ *
12
+ * `execute` resolves to the tool's value, unwrapped from the MCP call result:
13
+ * - any result with `isError: true` rejects with an Error whose message is
14
+ * the result's text content (every `content[].text`, joined by newlines);
15
+ * - a management tool's value is `result.structuredContent` (its text
16
+ * content is the same value as JSON and is never consulted);
17
+ * - an action tool's value is the whole MCP result (`content`,
18
+ * `structuredContent`, `truncated`) exactly as the driver returned it.
19
+ * A remote implementation sends `name`/`args` as an MCP `tools/call` and
20
+ * applies these three rules to the response.
21
+ */
22
+ interface ToolCommands {
23
+ execute(name: string, args: Record<string, unknown>): Promise<unknown>;
24
+ }
25
+ declare function createToolboxCommands(toolbox: Toolbox, loadout?: LoadoutInput): ToolCommands;
26
+ export { createToolboxCommands };
27
+ export type { ToolCommands };
@@ -0,0 +1,15 @@
1
+ import { isActionMcpName } from './ids.js';
2
+ import { callMcpTool } from './mcp.js';
3
+ function unwrap(name, result) {
4
+ if (result.isError)
5
+ throw new Error(result.content.map((item) => item.text).join('\n'));
6
+ return isActionMcpName(name) ? result : result.structuredContent;
7
+ }
8
+ function createToolboxCommands(toolbox, loadout) {
9
+ return {
10
+ async execute(name, args) {
11
+ return unwrap(name, await callMcpTool(toolbox, name, args, { loadout }));
12
+ },
13
+ };
14
+ }
15
+ export { createToolboxCommands };
@@ -140,6 +140,7 @@ function normalizeDefinition(definition, existing, now = new Date().toISOString(
140
140
  owner: definition.owner?.trim() || 'user', origin,
141
141
  status: definition.status || 'enabled',
142
142
  ...(definition.description?.trim() ? { description: definition.description.trim() } : {}),
143
+ ...(definition.appId?.trim() ? { appId: definition.appId.trim() } : {}),
143
144
  ...(definition.display ? { display: structuredClone(definition.display) } : {}),
144
145
  ...(definition.policy ? { policy: structuredClone(definition.policy) } : {}),
145
146
  ...(definition.metadata ? { metadata: structuredClone(definition.metadata) } : {}),
@@ -1,4 +1,20 @@
1
- import type { HostMcpToolDefinitionInput, ToolDefinition } from './types.js';
1
+ import type { JsonSchema, ToolDefinition } from './types.js';
2
+ interface HostMcpToolDescriptor {
3
+ name: string;
4
+ description?: string;
5
+ inputSchema?: JsonSchema;
6
+ }
7
+ interface HostMcpToolDefinitionInput {
8
+ id: string;
9
+ name: string;
10
+ description?: string;
11
+ serverName: string;
12
+ route: string;
13
+ tools: readonly HostMcpToolDescriptor[];
14
+ owner?: string;
15
+ display?: Record<string, unknown>;
16
+ metadata?: Record<string, unknown>;
17
+ }
2
18
  /**
3
19
  * Project an owning SDK's MCP descriptors into one immutable first-party
4
20
  * Toolbox tool. The descriptors remain authoritative; this adapter does not
@@ -6,3 +22,4 @@ import type { HostMcpToolDefinitionInput, ToolDefinition } from './types.js';
6
22
  */
7
23
  declare function defineHostMcpTool(input: HostMcpToolDefinitionInput): ToolDefinition;
8
24
  export { defineHostMcpTool };
25
+ export type { HostMcpToolDefinitionInput, HostMcpToolDescriptor };
package/dist/ids.d.ts CHANGED
@@ -3,5 +3,7 @@ declare function id(value: unknown, label?: string, maximumLength?: number): str
3
3
  declare function actionName(value: unknown): string;
4
4
  declare function actionId(toolId: string, name: string): string;
5
5
  declare function mcpName(action: Pick<ActionRecord, 'id'>): string;
6
+ /** Action tools carry the double-underscore prefix; management tools never do. */
7
+ declare function isActionMcpName(name: string): boolean;
6
8
  declare function assertMcpNamesUnique(actions: Pick<ActionRecord, 'id'>[]): void;
7
- export { actionId, actionName, assertMcpNamesUnique, id, mcpName };
9
+ export { actionId, actionName, assertMcpNamesUnique, id, isActionMcpName, mcpName };
package/dist/ids.js CHANGED
@@ -21,8 +21,13 @@ function actionId(toolId, name) {
21
21
  function safeMcpName(value) {
22
22
  return value.replace(/[^A-Za-z0-9_-]+/g, '_').replace(/_+/g, '_');
23
23
  }
24
+ const MCP_ACTION_PREFIX = 'toolbox__';
24
25
  function mcpName(action) {
25
- return `toolbox__${safeMcpName(action.id)}`;
26
+ return `${MCP_ACTION_PREFIX}${safeMcpName(action.id)}`;
27
+ }
28
+ /** Action tools carry the double-underscore prefix; management tools never do. */
29
+ function isActionMcpName(name) {
30
+ return name.startsWith(MCP_ACTION_PREFIX);
26
31
  }
27
32
  function assertMcpNamesUnique(actions) {
28
33
  const names = new Map();
@@ -35,4 +40,4 @@ function assertMcpNamesUnique(actions) {
35
40
  names.set(name, action.id);
36
41
  }
37
42
  }
38
- export { actionId, actionName, assertMcpNamesUnique, id, mcpName };
43
+ export { actionId, actionName, assertMcpNamesUnique, id, isActionMcpName, mcpName };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { ArtifactFiles } from './artifact-files.js';
2
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
3
  export type { ArtifactFileWrite, LegacyMigrationPlan, ToolArtifactDocument, ToolboxIndexDocument, } from './artifacts.js';
4
+ export { createToolboxCommands } from './commands.js';
5
+ export type { ToolCommands } from './commands.js';
4
6
  export { defineTool, normalizeDefinition } from './definition.js';
5
7
  export { defineHostMcpTool } from './host-mcp-tool.js';
6
8
  export { TOOL_DEPLOYMENT_CONTRACT, TOOL_DEPLOYMENT_KIND, TOOL_DEPLOYMENT_SCHEMA_VERSION, assertToolDeploymentActivation, createToolDeployment, deploymentRevisionId, emptyToolDeploymentSnapshot, } from './deployments.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { ArtifactFiles } from './artifact-files.js';
2
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 { createToolboxCommands } from './commands.js';
3
4
  export { defineTool, normalizeDefinition } from './definition.js';
4
5
  export { defineHostMcpTool } from './host-mcp-tool.js';
5
6
  export { TOOL_DEPLOYMENT_CONTRACT, TOOL_DEPLOYMENT_KIND, TOOL_DEPLOYMENT_SCHEMA_VERSION, assertToolDeploymentActivation, createToolDeployment, deploymentRevisionId, emptyToolDeploymentSnapshot, } from './deployments.js';
package/dist/mcp.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { Toolbox } from './toolbox.js';
2
2
  import type { JsonSchema, McpOptions, McpTool } from './types.js';
3
3
  declare function portable(schema: JsonSchema): JsonSchema;
4
4
  declare function managementToolDescriptors(): Array<Omit<McpTool, 'handler'>>;
5
- declare function managementTools(toolbox: Toolbox): McpTool[];
5
+ declare function managementTools(toolbox: Toolbox, options?: McpOptions): McpTool[];
6
6
  declare function actionTools(toolbox: Toolbox, options?: McpOptions): McpTool[];
7
7
  declare function createMcpTools(toolbox: Toolbox, options?: McpOptions): McpTool[];
8
8
  declare function findMcpTool(toolbox: Toolbox, name: string, options?: McpOptions): McpTool | null;
package/dist/mcp.js CHANGED
@@ -29,7 +29,12 @@ function managementToolDescriptors() {
29
29
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
30
30
  },
31
31
  {
32
- name: 'toolbox_tool_get', description: 'Get one tool and its actions.',
32
+ name: 'toolbox_tools_selected',
33
+ description: 'List tools with their selected actions: the loadout, or every enabled action.',
34
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
35
+ },
36
+ {
37
+ name: 'toolbox_tool_get', description: 'Get one tool and its actions, or one action.',
33
38
  inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
34
39
  },
35
40
  {
@@ -51,27 +56,41 @@ function managementToolDescriptors() {
51
56
  name: 'toolbox_record_remove', description: 'Remove one user-owned tool or action.',
52
57
  inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
53
58
  },
59
+ {
60
+ name: 'toolbox_tool_link', description: 'Link one user-owned tool to the app it belongs to.',
61
+ inputSchema: {
62
+ type: 'object', required: ['id', 'app_id'], additionalProperties: false,
63
+ properties: { ...idProperty, app_id: { type: 'string', description: 'App id' } },
64
+ },
65
+ },
66
+ {
67
+ name: 'toolbox_tool_unlink', description: 'Detach one user-owned tool from its app.',
68
+ inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
69
+ },
54
70
  {
55
71
  name: 'toolbox_mcp_connections', description: 'List external MCP connections for the composing host.',
56
72
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
57
73
  },
58
74
  ];
59
75
  }
60
- function managementTools(toolbox) {
61
- const [list, get, apply, enable, disable, remove, connections] = managementToolDescriptors();
76
+ function managementTools(toolbox, options = {}) {
77
+ const [list, selected, get, apply, enable, disable, remove, link, unlink, connections,] = managementToolDescriptors();
62
78
  return [
63
79
  managed(list, () => toolbox.catalog()),
80
+ managed(selected, () => toolbox.list(options.loadout)),
64
81
  managed(get, ({ id }) => {
65
- const result = toolbox.get(String(id));
82
+ const result = toolbox.get(String(id)) || toolbox.action(String(id));
66
83
  if (!result)
67
- throw new Error(`Unknown tool: ${id}`);
84
+ throw new Error(`Unknown tool or action: ${id}`);
68
85
  return result;
69
86
  }),
70
87
  managed(apply, ({ tool }) => toolbox.apply(tool)),
71
88
  managed(enable, ({ id }) => toolbox.setStatus(String(id), 'enabled')),
72
89
  managed(disable, ({ id }) => toolbox.setStatus(String(id), 'disabled')),
73
90
  managed(remove, ({ id }) => toolbox.remove(String(id))),
74
- managed(connections, () => toolbox.connections()),
91
+ managed(link, ({ id, app_id }) => toolbox.link(String(id), String(app_id))),
92
+ managed(unlink, ({ id }) => toolbox.link(String(id), null)),
93
+ managed(connections, () => toolbox.connections(options.loadout)),
75
94
  ];
76
95
  }
77
96
  function actionTools(toolbox, options = {}) {
@@ -91,7 +110,7 @@ function actionTools(toolbox, options = {}) {
91
110
  }
92
111
  function createMcpTools(toolbox, options = {}) {
93
112
  return [
94
- ...(options.includeManagement === false ? [] : managementTools(toolbox)),
113
+ ...(options.includeManagement === false ? [] : managementTools(toolbox, options)),
95
114
  ...actionTools(toolbox, options),
96
115
  ];
97
116
  }
package/dist/toolbox.d.ts CHANGED
@@ -34,8 +34,11 @@ declare class Toolbox {
34
34
  update(toolId: string, patch: ToolPatch): Promise<ApplyResult>;
35
35
  upsertAction(toolId: string, definition: ActionDefinition): Promise<ActionRecord>;
36
36
  updateAction(actionId: string, patch: ActionPatch): Promise<ActionRecord>;
37
- setStatus(recordId: string, status: 'enabled' | 'disabled'): Promise<ToolRecord | ActionRecord>;
38
- remove(recordId: string): Promise<ToolRecord | ActionRecord>;
37
+ setStatus(recordId: string, status: 'enabled' | 'disabled'): Promise<ActionRecord | ToolRecord>;
38
+ /** Link one user tool to the app it belongs to; `null` unlinks it. */
39
+ link(toolId: string, appId: string | null): Promise<ToolRecord>;
40
+ isSystemTool(toolId: string): boolean;
41
+ remove(recordId: string): Promise<ActionRecord | ToolRecord>;
39
42
  currentDeployment(toolId: string): ToolDeployment | null;
40
43
  deployment(deploymentId: string): ToolDeployment | null;
41
44
  deploymentHistory(toolId: string): ToolDeployment[];
package/dist/toolbox.js CHANGED
@@ -5,7 +5,7 @@ import { apiDriver } from './api-driver.js';
5
5
  import { applyDefinition } from './apply-definition.js';
6
6
  import { ArtifactFiles } from './artifact-files.js';
7
7
  import { preserveProjectionTime } from './artifacts.js';
8
- import { cliDriver } from './cli-driver.js';
8
+ import { createCliDriver } from './cli-driver.js';
9
9
  import { DeploymentFiles } from './deployment-files.js';
10
10
  import { deploymentRevisionId } from './deployments.js';
11
11
  import { normalizeDefinition } from './definition.js';
@@ -14,7 +14,7 @@ import { Store } from './store.js';
14
14
  import { ToolboxDeployments } from './toolbox-deployments.js';
15
15
  import { ToolboxProjection } from './toolbox-projection.js';
16
16
  import { ToolboxView } from './toolbox-view.js';
17
- import { removeAction, updateAction, updateTool, upsertAction } from './updates.js';
17
+ import { linkTool, removeAction, updateAction, updateTool, upsertAction } from './updates.js';
18
18
  function defaultDatabase(options) {
19
19
  if (options.databaseFile)
20
20
  return path.resolve(options.databaseFile);
@@ -43,7 +43,7 @@ class Toolbox {
43
43
  const artifacts = new ArtifactFiles(path.dirname(databaseFile));
44
44
  const deploymentFiles = new DeploymentFiles(path.resolve(options.deploymentDir || options.stateDir || path.dirname(databaseFile)));
45
45
  const priorIndex = artifacts.readIndex();
46
- for (const driver of [cliDriver, apiDriver, ...(options.drivers || [])])
46
+ for (const driver of [createCliDriver(options.environment), apiDriver, ...(options.drivers || [])])
47
47
  this.drivers.set(driver.type, driver);
48
48
  const tools = [];
49
49
  const actions = [];
@@ -138,6 +138,11 @@ class Toolbox {
138
138
  throw new Error(`Unknown tool or action: ${value}`);
139
139
  return action.status === status ? action : this.updateAction(value, { status });
140
140
  }
141
+ /** Link one user tool to the app it belongs to; `null` unlinks it. */
142
+ async link(toolId, appId) {
143
+ return linkTool(this, toolId, appId);
144
+ }
145
+ isSystemTool(toolId) { return this.system.tools.some((tool) => tool.id === toolId); }
141
146
  async remove(recordId) {
142
147
  const value = id(recordId, 'record id');
143
148
  if (this.system.tools.some((tool) => tool.id === value)
package/dist/types.d.ts CHANGED
@@ -75,6 +75,8 @@ interface ToolDefinition {
75
75
  id: string;
76
76
  name: string;
77
77
  description?: string;
78
+ /** The app this tool belongs to: the reverse edge of the app manifest's `toolIds`. */
79
+ appId?: string;
78
80
  owner?: string;
79
81
  origin?: Origin;
80
82
  status?: Status;
@@ -163,6 +165,8 @@ interface ToolboxOptions {
163
165
  deploymentDir?: string;
164
166
  systemTools?: ToolDefinition[];
165
167
  drivers?: ToolDriver[];
168
+ /** Host-supplied process environment for one CLI tool call, such as where its linked app listens. */
169
+ environment?: (tool: ToolRecord) => Record<string, string> | Promise<Record<string, string>>;
166
170
  onChange?: (catalog: Catalog) => void | Promise<void>;
167
171
  /** Outbound local Live operation. Remote application never calls it. */
168
172
  onDeployment?: (activation: ToolDeploymentActivation) => void | Promise<void>;
@@ -176,26 +180,11 @@ interface McpTool {
176
180
  interface McpOptions extends CallOptions {
177
181
  includeManagement?: boolean;
178
182
  }
179
- interface HostMcpToolDescriptor {
180
- name: string;
181
- description?: string;
182
- inputSchema?: JsonSchema;
183
- }
184
- interface HostMcpToolDefinitionInput {
185
- id: string;
186
- name: string;
187
- description?: string;
188
- serverName: string;
189
- route: string;
190
- tools: readonly HostMcpToolDescriptor[];
191
- owner?: string;
192
- display?: Record<string, unknown>;
193
- metadata?: Record<string, unknown>;
194
- }
195
183
  interface CliIo {
196
184
  stdout?: Writable;
197
185
  stderr?: Writable;
198
- toolbox?: import('./toolbox.js').Toolbox;
186
+ commands?: import('./commands.js').ToolCommands;
199
187
  }
200
- export type { ActionDefinition, ActionRecord, ActionTarget, ApiSource, ApiTarget, ApplyResult, CallOptions, Catalog, CliIo, CliSource, CliTarget, DriverCall, HostMcpToolDefinitionInput, HostMcpToolDescriptor, JsonSchema, Loadout, LoadoutInput, McpOptions, McpSource, McpTarget, McpTool, Origin, SecretResolver, Status, ToolDefinition, ToolDriver, ToolboxOptions, ToolRecord, ToolResult, ToolPatch, ActionPatch, ToolQuery, ToolSource, ToolType, };
188
+ export type { ActionDefinition, ActionRecord, ActionTarget, ApiSource, ApiTarget, ApplyResult, CallOptions, Catalog, CliIo, CliSource, CliTarget, DriverCall, JsonSchema, Loadout, LoadoutInput, McpOptions, McpSource, McpTarget, McpTool, Origin, SecretResolver, Status, ToolDefinition, ToolDriver, ToolboxOptions, ToolRecord, ToolResult, ToolPatch, ActionPatch, ToolQuery, ToolSource, ToolType, };
201
189
  export type { ToolDeployment, ToolDeploymentActivation, ToolDeploymentSnapshot, ToolDeploymentSurface, } from './deployment-types.js';
190
+ export type { HostMcpToolDefinitionInput, HostMcpToolDescriptor } from './host-mcp-tool.js';
package/dist/updates.d.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  import type { Toolbox } from './toolbox.js';
2
- import type { ActionDefinition, ActionPatch, ActionRecord, ApplyResult, ToolPatch } from './types.js';
2
+ import type { ActionDefinition, ActionPatch, ActionRecord, ApplyResult, ToolPatch, ToolRecord } from './types.js';
3
3
  type UpdatePort = Pick<Toolbox, 'action' | 'apply' | 'get'>;
4
+ type LinkPort = UpdatePort & Pick<Toolbox, 'update'> & {
5
+ isSystemTool(toolId: string): boolean;
6
+ };
4
7
  declare function updateTool(toolbox: UpdatePort, toolId: string, patch: ToolPatch): Promise<ApplyResult>;
8
+ /** Link one user tool to its app; `null` unlinks. The record is unchanged when the link already holds. */
9
+ declare function linkTool(toolbox: LinkPort, toolId: string, appId: string | null): Promise<ToolRecord>;
5
10
  declare function upsertAction(toolbox: UpdatePort, toolId: string, definition: ActionDefinition): Promise<ActionRecord>;
6
11
  declare function updateAction(toolbox: UpdatePort, actionId: string, patch: ActionPatch): Promise<ActionRecord>;
7
12
  declare function removeAction(toolbox: UpdatePort, actionId: string): Promise<ActionRecord>;
8
- export { removeAction, updateAction, updateTool, upsertAction };
13
+ export { linkTool, removeAction, updateAction, updateTool, upsertAction };
package/dist/updates.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { id } from './ids.js';
1
2
  function actionDefinition(action) {
2
3
  const { id, toolId: _toolId, createdAt: _createdAt, updatedAt: _updatedAt, ...definition } = action;
3
4
  return { ...structuredClone(definition), id };
@@ -25,6 +26,19 @@ async function updateTool(toolbox, toolId, patch) {
25
26
  actions: patch.actions === undefined ? definition.actions : patch.actions,
26
27
  });
27
28
  }
29
+ /** Link one user tool to its app; `null` unlinks. The record is unchanged when the link already holds. */
30
+ async function linkTool(toolbox, toolId, appId) {
31
+ const value = id(toolId, 'tool id');
32
+ if (toolbox.isSystemTool(value))
33
+ throw new Error(`System record is immutable: ${value}`);
34
+ const current = toolbox.get(value);
35
+ if (!current)
36
+ throw new Error(`Unknown tool: ${value}`);
37
+ const trimmed = appId?.trim() || null;
38
+ if ((current.tool.appId ?? null) === trimmed)
39
+ return current.tool;
40
+ return (await toolbox.update(value, { appId: trimmed ?? undefined })).tool;
41
+ }
28
42
  async function upsertAction(toolbox, toolId, definition) {
29
43
  const current = toolbox.get(toolId);
30
44
  if (!current)
@@ -65,4 +79,4 @@ async function removeAction(toolbox, actionId) {
65
79
  });
66
80
  return action;
67
81
  }
68
- export { removeAction, updateAction, updateTool, upsertAction };
82
+ export { linkTool, removeAction, updateAction, updateTool, upsertAction };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amalgm/tools",
3
- "version": "0.1.7-preview.34436591947",
3
+ "version": "0.1.7-preview.34547098528",
4
4
  "description": "Local-first tool definitions, Toolbox registry, and agent execution surfaces.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -87,11 +87,11 @@
87
87
  "package.json"
88
88
  ]
89
89
  },
90
- "gitHead": "9a662da6b5fe55eceffd52aac34cdf954e015b07",
90
+ "gitHead": "16bda6108cbbd03c67fa4d6892a7b24b58e343e3",
91
91
  "amalgmSource": {
92
92
  "repository": "amalgm-inc/amalgm-tools",
93
93
  "branch": "preview",
94
- "commit": "9a662da6b5fe55eceffd52aac34cdf954e015b07",
95
- "occurrence": "34436591947"
94
+ "commit": "16bda6108cbbd03c67fa4d6892a7b24b58e343e3",
95
+ "occurrence": "34547098528"
96
96
  }
97
97
  }
@@ -144,10 +144,16 @@ amalgm-tools show hello.say
144
144
  amalgm-tools enable hello.say
145
145
  amalgm-tools disable hello.say
146
146
  amalgm-tools remove hello
147
+ amalgm-tools link canvas app-3f1c
148
+ amalgm-tools unlink canvas
147
149
  amalgm-tools run hello.say --input '{"name":"Ada"}'
148
150
  amalgm-tools connections
149
151
  ```
150
152
 
153
+ `link` records the app a tool belongs to (`appId`, the reverse edge of the app
154
+ manifest's `toolIds`); sharing the app carries the tool and sharing the tool
155
+ carries the app. The MCP twins are `toolbox_tool_link` and `toolbox_tool_unlink`.
156
+
151
157
  Use `--state-dir DIR` to select a Toolbox and `--loadout one,two` to bound
152
158
  `list`, `run`, or connection projection.
153
159