@amalgm/tools 0.1.2 → 0.1.4

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/dist/mcp.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  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
+ declare function managementToolDescriptors(): Array<Omit<McpTool, 'handler'>>;
4
5
  declare function managementTools(toolbox: Toolbox): McpTool[];
5
6
  declare function actionTools(toolbox: Toolbox, options?: McpOptions): McpTool[];
6
7
  declare function createMcpTools(toolbox: Toolbox, options?: McpOptions): McpTool[];
7
8
  declare function findMcpTool(toolbox: Toolbox, name: string, options?: McpOptions): McpTool | null;
8
9
  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, };
10
+ export { actionTools, callMcpTool, createMcpTools, findMcpTool, managementTools, managementToolDescriptors, portable, };
package/dist/mcp.js CHANGED
@@ -21,45 +21,57 @@ function managed(definition, handler) {
21
21
  },
22
22
  };
23
23
  }
24
- function managementTools(toolbox) {
24
+ function managementToolDescriptors() {
25
25
  const idProperty = { id: { type: 'string', description: 'Tool or action id' } };
26
26
  return [
27
- managed({
27
+ {
28
28
  name: 'toolbox_tools_list', description: 'List the canonical Toolbox catalog.',
29
29
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
30
- }, () => toolbox.catalog()),
31
- managed({
30
+ },
31
+ {
32
32
  name: 'toolbox_tool_get', description: 'Get one tool and its actions.',
33
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({
34
+ },
35
+ {
41
36
  name: 'toolbox_tool_apply', description: 'Atomically apply an authoritative tool definition.',
42
37
  inputSchema: {
43
38
  type: 'object', required: ['tool'], additionalProperties: false,
44
39
  properties: { tool: { type: 'object', description: 'Complete tool definition' } },
45
40
  },
46
- }, ({ tool }) => toolbox.apply(tool)),
47
- managed({
41
+ },
42
+ {
48
43
  name: 'toolbox_record_enable', description: 'Enable one user-owned tool or action.',
49
44
  inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
50
- }, ({ id }) => toolbox.setStatus(String(id), 'enabled')),
51
- managed({
45
+ },
46
+ {
52
47
  name: 'toolbox_record_disable', description: 'Disable one user-owned tool or action.',
53
48
  inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
54
- }, ({ id }) => toolbox.setStatus(String(id), 'disabled')),
55
- managed({
49
+ },
50
+ {
56
51
  name: 'toolbox_record_remove', description: 'Remove one user-owned tool or action.',
57
52
  inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
58
- }, ({ id }) => toolbox.remove(String(id))),
59
- managed({
53
+ },
54
+ {
60
55
  name: 'toolbox_mcp_connections', description: 'List external MCP connections for the composing host.',
61
56
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
62
- }, () => toolbox.connections()),
57
+ },
58
+ ];
59
+ }
60
+ function managementTools(toolbox) {
61
+ const [list, get, apply, enable, disable, remove, connections] = managementToolDescriptors();
62
+ return [
63
+ managed(list, () => toolbox.catalog()),
64
+ managed(get, ({ id }) => {
65
+ const result = toolbox.get(String(id));
66
+ if (!result)
67
+ throw new Error(`Unknown tool: ${id}`);
68
+ return result;
69
+ }),
70
+ managed(apply, ({ tool }) => toolbox.apply(tool)),
71
+ managed(enable, ({ id }) => toolbox.setStatus(String(id), 'enabled')),
72
+ managed(disable, ({ id }) => toolbox.setStatus(String(id), 'disabled')),
73
+ managed(remove, ({ id }) => toolbox.remove(String(id))),
74
+ managed(connections, () => toolbox.connections()),
63
75
  ];
64
76
  }
65
77
  function actionTools(toolbox, options = {}) {
@@ -92,4 +104,4 @@ async function callMcpTool(toolbox, name, input = {}, options = {}) {
92
104
  throw new Error(`Unknown tool: ${name}`);
93
105
  return tool.handler(input);
94
106
  }
95
- export { actionTools, callMcpTool, createMcpTools, findMcpTool, managementTools, portable, };
107
+ export { actionTools, callMcpTool, createMcpTools, findMcpTool, managementTools, managementToolDescriptors, portable, };
@@ -18,11 +18,12 @@ function contextSessionId(value) {
18
18
  function notificationToolDefinition() {
19
19
  return {
20
20
  id: 'notifications',
21
- name: 'Notifications',
21
+ name: 'Channels',
22
22
  description: 'Notify the current user about important results, completions, and failures.',
23
23
  owner: 'amalgm',
24
24
  origin: 'system',
25
- source: { type: 'mcp', transport: 'host', serverName: 'notifications' },
25
+ source: { type: 'mcp', transport: 'host', serverName: 'channels', route: '/mcp/channels' },
26
+ policy: { firstParty: true },
26
27
  actions: [{
27
28
  name: 'notify_user',
28
29
  description: 'Send the current user an email notification. Keep it concise and actionable.',
@@ -91,7 +92,7 @@ function createNotificationsDriver(notifications) {
91
92
  return tool.id === 'notifications'
92
93
  && tool.source.type === 'mcp'
93
94
  && tool.source.transport === 'host'
94
- && tool.source.serverName === 'notifications';
95
+ && tool.source.serverName === 'channels';
95
96
  },
96
97
  async call({ action, input, options }) {
97
98
  if (action.id !== 'notifications.notify_user')
package/dist/schema.js CHANGED
@@ -21,6 +21,21 @@ function migrate(db) {
21
21
  UNIQUE(tool_id, id)
22
22
  );
23
23
  CREATE INDEX IF NOT EXISTS actions_tool_id ON actions(tool_id);
24
+
25
+ CREATE TABLE IF NOT EXISTS tool_deployments (
26
+ id TEXT PRIMARY KEY,
27
+ tool_id TEXT NOT NULL,
28
+ previous_deployment_id TEXT,
29
+ created_at TEXT NOT NULL,
30
+ record_json TEXT NOT NULL
31
+ );
32
+ CREATE INDEX IF NOT EXISTS tool_deployments_tool_id
33
+ ON tool_deployments(tool_id, created_at, id);
34
+
35
+ CREATE TABLE IF NOT EXISTS tool_heads (
36
+ tool_id TEXT PRIMARY KEY,
37
+ deployment_id TEXT NOT NULL REFERENCES tool_deployments(id)
38
+ );
24
39
  `);
25
40
  }
26
41
  export { migrate };
package/dist/store.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import Database from 'better-sqlite3';
2
- import type { ActionRecord, ApplyResult, Catalog, Status, ToolRecord } from './types.js';
2
+ import type { ActionRecord, ApplyResult, Catalog, ToolDeployment, ToolDeploymentActivation, ToolDeploymentSnapshot, ToolRecord } from './types.js';
3
3
  declare class Store {
4
4
  readonly db: Database.Database;
5
5
  constructor(file: string);
@@ -8,10 +8,23 @@ declare class Store {
8
8
  getTool(id: string): ToolRecord | null;
9
9
  getAction(id: string): ActionRecord | null;
10
10
  actionsFor(toolId: string): ActionRecord[];
11
+ heads(): Record<string, string>;
11
12
  catalog(): Catalog;
12
- apply(result: ApplyResult): ApplyResult;
13
- setStatus(id: string, status: Status, now: string): ToolRecord | ActionRecord | null;
14
- remove(id: string): ToolRecord | ActionRecord | null;
13
+ currentDeploymentId(toolId: string): string | null;
14
+ getDeployment(id: string): ToolDeployment | null;
15
+ deploymentsFor(toolId: string): ToolDeployment[];
16
+ allDeployments(): ToolDeployment[];
17
+ deploymentSnapshot(): ToolDeploymentSnapshot;
18
+ /** Add a baseline for a pre-deployment database without changing its catalog bytes. */
19
+ adoptDeployment(activation: ToolDeploymentActivation): void;
20
+ applyDeployment(activation: ToolDeploymentActivation): {
21
+ changed: boolean;
22
+ result: ApplyResult | null;
23
+ };
24
+ replaceDeploymentSnapshot(snapshot: ToolDeploymentSnapshot): void;
25
+ private insertDeployment;
26
+ private setHead;
27
+ private materializeDefinition;
15
28
  close(): void;
16
29
  }
17
30
  export { Store };
package/dist/store.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import Database from 'better-sqlite3';
4
+ import { canonicalJson, deploymentRevisionId } from './deployments.js';
4
5
  import { migrate } from './schema.js';
5
6
  function parse(row) {
6
7
  return row ? JSON.parse(row.record_json) : null;
@@ -32,65 +33,128 @@ class Store {
32
33
  return this.db.prepare('SELECT record_json FROM actions WHERE tool_id = ? ORDER BY id')
33
34
  .all(toolId).map((row) => parse(row)).filter(Boolean);
34
35
  }
36
+ heads() {
37
+ return Object.fromEntries(this.db.prepare('SELECT tool_id, deployment_id FROM tool_heads ORDER BY tool_id').all()
38
+ .map((row) => [row.tool_id, row.deployment_id]));
39
+ }
35
40
  catalog() {
36
41
  const tools = this.db.prepare('SELECT record_json FROM tools ORDER BY id')
37
42
  .all().map((row) => parse(row)).filter(Boolean);
38
43
  const actions = this.db.prepare('SELECT record_json FROM actions ORDER BY id')
39
44
  .all().map((row) => parse(row)).filter(Boolean);
40
- return { version: 1, revision: this.revision(), tools, actions };
45
+ const deployments = this.heads();
46
+ return {
47
+ version: 1,
48
+ revision: this.revision(),
49
+ revisionId: deploymentRevisionId({ heads: deployments }),
50
+ deployments,
51
+ tools,
52
+ actions,
53
+ };
54
+ }
55
+ currentDeploymentId(toolId) {
56
+ const row = this.db.prepare('SELECT deployment_id FROM tool_heads WHERE tool_id = ?').get(toolId);
57
+ return row?.deployment_id || null;
58
+ }
59
+ getDeployment(id) {
60
+ return parse(this.db.prepare('SELECT record_json FROM tool_deployments WHERE id = ?').get(id));
61
+ }
62
+ deploymentsFor(toolId) {
63
+ return this.db.prepare(`
64
+ SELECT record_json FROM tool_deployments
65
+ WHERE tool_id = ? ORDER BY created_at, id
66
+ `).all(toolId).map((row) => parse(row)).filter(Boolean);
67
+ }
68
+ allDeployments() {
69
+ return this.db.prepare(`
70
+ SELECT record_json FROM tool_deployments ORDER BY created_at, id
71
+ `).all().map((row) => parse(row)).filter(Boolean);
72
+ }
73
+ deploymentSnapshot() {
74
+ return {
75
+ version: 1,
76
+ deployments: Object.fromEntries(this.allDeployments().map((deployment) => [deployment.id, deployment])),
77
+ heads: this.heads(),
78
+ };
41
79
  }
42
- apply(result) {
80
+ /** Add a baseline for a pre-deployment database without changing its catalog bytes. */
81
+ adoptDeployment(activation) {
82
+ const deployment = activation.deployment;
43
83
  this.db.transaction(() => {
44
- this.db.prepare(`
45
- INSERT INTO tools (id, origin, status, record_json)
46
- VALUES (@id, @origin, @status, @json)
47
- ON CONFLICT(id) DO UPDATE SET
48
- origin = excluded.origin, status = excluded.status, record_json = excluded.record_json
49
- `).run({ id: result.tool.id, origin: result.tool.origin, status: result.tool.status, json: JSON.stringify(result.tool) });
50
- this.db.prepare('DELETE FROM actions WHERE tool_id = ?').run(result.tool.id);
51
- const insert = this.db.prepare('INSERT INTO actions (id, tool_id, status, record_json) VALUES (?, ?, ?, ?)');
52
- for (const action of result.actions)
53
- insert.run(action.id, action.toolId, action.status, JSON.stringify(action));
54
- this.bump();
84
+ if (this.getDeployment(deployment.id))
85
+ return;
86
+ this.insertDeployment(deployment);
87
+ this.setHead(deployment.subjectId, deployment.id);
55
88
  })();
56
- return result;
57
89
  }
58
- setStatus(id, status, now) {
90
+ applyDeployment(activation) {
91
+ const deployment = activation.deployment;
59
92
  return this.db.transaction(() => {
60
- const tool = this.getTool(id);
61
- if (tool) {
62
- const updated = { ...tool, status, updatedAt: now };
63
- this.db.prepare('UPDATE tools SET status = ?, record_json = ? WHERE id = ?')
64
- .run(status, JSON.stringify(updated), id);
65
- this.bump();
66
- return updated;
93
+ const existing = this.getDeployment(deployment.id);
94
+ if (existing) {
95
+ if (canonicalJson(existing) !== canonicalJson(deployment)) {
96
+ throw new Error(`Deployment id is immutable: ${deployment.id}`);
97
+ }
98
+ return { changed: false, result: existing.definition };
67
99
  }
68
- const action = this.getAction(id);
69
- if (!action)
70
- return null;
71
- const updated = { ...action, status, updatedAt: now };
72
- this.db.prepare('UPDATE actions SET status = ?, record_json = ? WHERE id = ?')
73
- .run(status, JSON.stringify(updated), id);
100
+ this.insertDeployment(deployment);
101
+ this.materializeDefinition(deployment.subjectId, deployment.definition);
102
+ this.setHead(deployment.subjectId, deployment.id);
74
103
  this.bump();
75
- return updated;
104
+ return { changed: true, result: deployment.definition };
76
105
  })();
77
106
  }
78
- remove(id) {
79
- return this.db.transaction(() => {
80
- const tool = this.getTool(id);
81
- if (tool) {
82
- this.db.prepare('DELETE FROM tools WHERE id = ?').run(id);
83
- this.bump();
84
- return tool;
107
+ replaceDeploymentSnapshot(snapshot) {
108
+ this.db.transaction(() => {
109
+ this.db.prepare('DELETE FROM actions').run();
110
+ this.db.prepare('DELETE FROM tools').run();
111
+ this.db.prepare('DELETE FROM tool_heads').run();
112
+ this.db.prepare('DELETE FROM tool_deployments').run();
113
+ for (const deployment of Object.values(snapshot.deployments))
114
+ this.insertDeployment(deployment);
115
+ for (const [toolId, deploymentId] of Object.entries(snapshot.heads)) {
116
+ const deployment = snapshot.deployments[deploymentId];
117
+ this.materializeDefinition(toolId, deployment.definition);
118
+ this.setHead(toolId, deploymentId);
85
119
  }
86
- const action = this.getAction(id);
87
- if (!action)
88
- return null;
89
- this.db.prepare('DELETE FROM actions WHERE id = ?').run(id);
90
120
  this.bump();
91
- return action;
92
121
  })();
93
122
  }
123
+ insertDeployment(deployment) {
124
+ this.db.prepare(`
125
+ INSERT INTO tool_deployments
126
+ (id, tool_id, previous_deployment_id, created_at, record_json)
127
+ VALUES (?, ?, ?, ?, ?)
128
+ `).run(deployment.id, deployment.subjectId, deployment.previousDeploymentId, deployment.createdAt, JSON.stringify(deployment));
129
+ }
130
+ setHead(toolId, deploymentId) {
131
+ this.db.prepare(`
132
+ INSERT INTO tool_heads (tool_id, deployment_id) VALUES (?, ?)
133
+ ON CONFLICT(tool_id) DO UPDATE SET deployment_id = excluded.deployment_id
134
+ `).run(toolId, deploymentId);
135
+ }
136
+ materializeDefinition(toolId, result) {
137
+ if (result === null) {
138
+ this.db.prepare('DELETE FROM tools WHERE id = ?').run(toolId);
139
+ return;
140
+ }
141
+ this.db.prepare(`
142
+ INSERT INTO tools (id, origin, status, record_json)
143
+ VALUES (@id, @origin, @status, @json)
144
+ ON CONFLICT(id) DO UPDATE SET
145
+ origin = excluded.origin, status = excluded.status, record_json = excluded.record_json
146
+ `).run({
147
+ id: result.tool.id,
148
+ origin: result.tool.origin,
149
+ status: result.tool.status,
150
+ json: JSON.stringify(result.tool),
151
+ });
152
+ this.db.prepare('DELETE FROM actions WHERE tool_id = ?').run(result.tool.id);
153
+ const insert = this.db.prepare('INSERT INTO actions (id, tool_id, status, record_json) VALUES (?, ?, ?, ?)');
154
+ for (const action of result.actions) {
155
+ insert.run(action.id, action.toolId, action.status, JSON.stringify(action));
156
+ }
157
+ }
94
158
  close() { this.db.close(); }
95
159
  }
96
160
  export { Store };
@@ -0,0 +1,30 @@
1
+ import { DeploymentFiles } from './deployment-files.js';
2
+ import { Store } from './store.js';
3
+ import type { ActionRecord, ApplyResult, ToolDeployment, ToolDeploymentActivation, ToolDeploymentSnapshot, ToolDeploymentSurface, ToolRecord, ToolboxOptions } from './types.js';
4
+ interface DeploymentServiceOptions {
5
+ store: Store;
6
+ files: DeploymentFiles;
7
+ systemTools: readonly ToolRecord[];
8
+ systemActions: readonly ActionRecord[];
9
+ project(toolId: string): Promise<void>;
10
+ replaceProjection(previousToolIds: readonly string[]): Promise<void>;
11
+ onDeployment?: ToolboxOptions['onDeployment'];
12
+ }
13
+ /** The Toolbox deployment state machine; adapters only observe its operations. */
14
+ declare class ToolboxDeployments {
15
+ private readonly options;
16
+ private readonly observers;
17
+ constructor(options: DeploymentServiceOptions);
18
+ adoptLegacy(): void;
19
+ activateLocal(definition: ApplyResult | null, deletedToolId?: string): Promise<void>;
20
+ current(toolId: string): ToolDeployment | null;
21
+ get(deploymentId: string): ToolDeployment | null;
22
+ history(toolId: string): ToolDeployment[];
23
+ snapshot(): ToolDeploymentSnapshot;
24
+ activateRemote(activation: ToolDeploymentActivation): Promise<ApplyResult | null>;
25
+ applySnapshot(snapshot: ToolDeploymentSnapshot): Promise<void>;
26
+ surface(): ToolDeploymentSurface;
27
+ private validateActivation;
28
+ private assertNotSystem;
29
+ }
30
+ export { ToolboxDeployments };
@@ -0,0 +1,125 @@
1
+ import { TOOL_DEPLOYMENT_CONTRACT, assertToolDeploymentActivation, createToolDeployment, } from './deployments.js';
2
+ import { assertMcpNamesUnique, id } from './ids.js';
3
+ /** The Toolbox deployment state machine; adapters only observe its operations. */
4
+ class ToolboxDeployments {
5
+ options;
6
+ observers = new Set();
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ adoptLegacy() {
11
+ const { store, systemTools, files } = this.options;
12
+ for (const tool of store.catalog().tools) {
13
+ if (store.currentDeploymentId(tool.id) || systemTools.some((system) => system.id === tool.id))
14
+ continue;
15
+ store.adoptDeployment(createToolDeployment({
16
+ toolId: tool.id,
17
+ previousDeploymentId: null,
18
+ definition: { tool, actions: store.actionsFor(tool.id) },
19
+ createdAt: tool.updatedAt,
20
+ }));
21
+ }
22
+ files.materialize(store.allDeployments());
23
+ }
24
+ async activateLocal(definition, deletedToolId) {
25
+ const toolId = definition?.tool.id || deletedToolId;
26
+ if (!toolId)
27
+ throw new Error('Tool deployment requires a stable tool id');
28
+ const activation = createToolDeployment({
29
+ toolId,
30
+ previousDeploymentId: this.options.store.currentDeploymentId(toolId),
31
+ definition,
32
+ createdAt: definition?.tool.updatedAt || new Date().toISOString(),
33
+ });
34
+ const applied = this.options.store.applyDeployment(activation);
35
+ if (!applied.changed)
36
+ return;
37
+ this.options.files.write(activation.deployment);
38
+ await this.options.project(toolId);
39
+ for (const observer of this.observers)
40
+ observer(activation);
41
+ await this.options.onDeployment?.(activation);
42
+ }
43
+ current(toolId) {
44
+ const value = id(toolId, 'tool id');
45
+ const deploymentId = this.options.store.currentDeploymentId(value);
46
+ return deploymentId ? this.options.store.getDeployment(deploymentId) : null;
47
+ }
48
+ get(deploymentId) {
49
+ return this.options.store.getDeployment(id(deploymentId, 'deployment id', 96));
50
+ }
51
+ history(toolId) {
52
+ return this.options.store.deploymentsFor(id(toolId, 'tool id'));
53
+ }
54
+ snapshot() {
55
+ return this.options.store.deploymentSnapshot();
56
+ }
57
+ async activateRemote(activation) {
58
+ this.validateActivation(activation);
59
+ const applied = this.options.store.applyDeployment(activation);
60
+ if (!applied.changed)
61
+ return applied.result;
62
+ this.options.files.write(activation.deployment);
63
+ await this.options.project(activation.deployment.subjectId);
64
+ return applied.result;
65
+ }
66
+ async applySnapshot(snapshot) {
67
+ if (!snapshot || snapshot.version !== 1 || !snapshot.deployments || !snapshot.heads) {
68
+ throw new Error('Invalid tool deployment snapshot');
69
+ }
70
+ for (const [deploymentId, deployment] of Object.entries(snapshot.deployments)) {
71
+ this.validateActivation({ type: 'deployment.activate', deployment }, false);
72
+ if (deploymentId !== deployment.id)
73
+ throw new Error(`Deployment snapshot key mismatch: ${deploymentId}`);
74
+ }
75
+ const currentDefinitions = [];
76
+ for (const [toolId, deploymentId] of Object.entries(snapshot.heads)) {
77
+ const deployment = snapshot.deployments[deploymentId];
78
+ if (!deployment || deployment.subjectId !== toolId) {
79
+ throw new Error(`Deployment snapshot head is invalid: ${toolId}`);
80
+ }
81
+ this.assertNotSystem(toolId);
82
+ if (deployment.definition)
83
+ currentDefinitions.push(deployment.definition);
84
+ }
85
+ assertMcpNamesUnique([
86
+ ...this.options.systemActions,
87
+ ...currentDefinitions.flatMap((definition) => definition.actions),
88
+ ]);
89
+ const previousToolIds = this.options.store.catalog().tools.map((tool) => tool.id);
90
+ this.options.store.replaceDeploymentSnapshot(snapshot);
91
+ this.options.files.materialize(Object.values(snapshot.deployments));
92
+ await this.options.replaceProjection(previousToolIds);
93
+ }
94
+ surface() {
95
+ return Object.freeze({
96
+ contract: TOOL_DEPLOYMENT_CONTRACT,
97
+ observeEdits: (emit) => {
98
+ this.observers.add(emit);
99
+ return () => this.observers.delete(emit);
100
+ },
101
+ applyRemote: async (operation) => {
102
+ await this.activateRemote(operation);
103
+ },
104
+ applySnapshot: (snapshot) => this.applySnapshot(snapshot),
105
+ });
106
+ }
107
+ validateActivation(activation, checkNames = true) {
108
+ assertToolDeploymentActivation(activation);
109
+ const deployment = activation.deployment;
110
+ this.assertNotSystem(deployment.subjectId);
111
+ if (checkNames && deployment.definition) {
112
+ assertMcpNamesUnique([
113
+ ...this.options.systemActions,
114
+ ...this.options.store.catalog().actions.filter((action) => action.toolId !== deployment.subjectId),
115
+ ...deployment.definition.actions,
116
+ ]);
117
+ }
118
+ }
119
+ assertNotSystem(toolId) {
120
+ if (this.options.systemTools.some((tool) => tool.id === toolId)) {
121
+ throw new Error(`System tool is immutable: ${toolId}`);
122
+ }
123
+ }
124
+ }
125
+ export { ToolboxDeployments };
@@ -0,0 +1,13 @@
1
+ import { ArtifactFiles } from './artifact-files.js';
2
+ import type { Catalog, ToolboxOptions } from './types.js';
3
+ /** Current-catalog compatibility projections; deployments remain authority. */
4
+ declare class ToolboxProjection {
5
+ private readonly artifacts;
6
+ private readonly catalog;
7
+ private readonly onChange?;
8
+ constructor(artifacts: ArtifactFiles, catalog: () => Catalog, onChange?: ToolboxOptions['onChange']);
9
+ boot(): void;
10
+ changed(toolId: string): Promise<void>;
11
+ replaced(previousToolIds: readonly string[]): Promise<void>;
12
+ }
13
+ export { ToolboxProjection };
@@ -0,0 +1,41 @@
1
+ /** Current-catalog compatibility projections; deployments remain authority. */
2
+ class ToolboxProjection {
3
+ artifacts;
4
+ catalog;
5
+ onChange;
6
+ constructor(artifacts, catalog, onChange) {
7
+ this.artifacts = artifacts;
8
+ this.catalog = catalog;
9
+ this.onChange = onChange;
10
+ }
11
+ boot() {
12
+ this.artifacts.migrateLegacy();
13
+ this.artifacts.materialize(this.catalog());
14
+ }
15
+ async changed(toolId) {
16
+ const catalog = this.catalog();
17
+ const tool = catalog.tools.find((candidate) => candidate.id === toolId);
18
+ if (tool && tool.origin !== 'system') {
19
+ this.artifacts.writeTool({
20
+ tool,
21
+ actions: catalog.actions.filter((action) => action.toolId === toolId),
22
+ });
23
+ }
24
+ else if (!tool) {
25
+ this.artifacts.removeTool(toolId);
26
+ }
27
+ this.artifacts.writeIndex(catalog);
28
+ await this.onChange?.(catalog);
29
+ }
30
+ async replaced(previousToolIds) {
31
+ const catalog = this.catalog();
32
+ const currentToolIds = new Set(catalog.tools.map((tool) => tool.id));
33
+ for (const toolId of previousToolIds) {
34
+ if (!currentToolIds.has(toolId))
35
+ this.artifacts.removeTool(toolId);
36
+ }
37
+ this.artifacts.materialize(catalog);
38
+ await this.onChange?.(catalog);
39
+ }
40
+ }
41
+ export { ToolboxProjection };
@@ -0,0 +1,35 @@
1
+ import type { ActionRecord, ApplyResult, CallOptions, Catalog, LoadoutInput, ToolDriver, ToolQuery, ToolRecord, ToolResult } from './types.js';
2
+ declare class ToolboxView {
3
+ private readonly readCatalog;
4
+ private readonly drivers;
5
+ constructor(readCatalog: () => Catalog, drivers: ReadonlyMap<string, ToolDriver>);
6
+ catalog(): Catalog;
7
+ list(loadout?: LoadoutInput): Array<{
8
+ tool: ToolRecord;
9
+ actions: ActionRecord[];
10
+ }>;
11
+ query(filters?: ToolQuery): {
12
+ tool: ToolRecord;
13
+ actions: ActionRecord[];
14
+ }[];
15
+ callable(loadout?: LoadoutInput, filters?: ToolQuery): {
16
+ tool: ToolRecord;
17
+ action: ActionRecord;
18
+ }[];
19
+ get(toolId: string): ApplyResult | null;
20
+ action(actionId: string): ActionRecord | null;
21
+ resolve(toolId: string, actionReference: string, loadout?: LoadoutInput): {
22
+ tool: ToolRecord;
23
+ action: ActionRecord;
24
+ } | null;
25
+ resolveCallable(toolId: string, actionReference: string, loadout?: LoadoutInput, filters?: ToolQuery): {
26
+ tool: ToolRecord;
27
+ action: ActionRecord;
28
+ } | null;
29
+ connections(loadout?: LoadoutInput): ToolRecord[];
30
+ call(actionId: string, input?: unknown, options?: CallOptions): Promise<ToolResult>;
31
+ hasDriver(tool: ToolRecord): boolean;
32
+ callMcp(name: string, input?: unknown, options?: CallOptions): Promise<ToolResult>;
33
+ private driverFor;
34
+ }
35
+ export { ToolboxView };