amalgm 0.1.72 → 0.1.73
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/package.json +1 -2
- package/runtime/scripts/amalgm-mcp/agents/rest.js +29 -22
- package/runtime/scripts/amalgm-mcp/agents/store.js +90 -24
- package/runtime/scripts/amalgm-mcp/agents/talk.js +66 -37
- package/runtime/scripts/amalgm-mcp/agents/tools.js +0 -4
- package/runtime/scripts/amalgm-mcp/automations/runner.js +4 -5
- package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +54 -57
- package/runtime/scripts/amalgm-mcp/automations/tools.js +4 -4
- package/runtime/scripts/amalgm-mcp/email/inbound.js +21 -0
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +3 -3
- package/runtime/scripts/amalgm-mcp/index.js +3 -1
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +15 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +5 -3
- package/runtime/scripts/amalgm-mcp/runtime-worker.js +47 -0
- package/runtime/scripts/amalgm-mcp/server/combined-service.js +27 -0
- package/runtime/scripts/amalgm-mcp/server/control-plane-service.js +24 -0
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +52 -7
- package/runtime/scripts/amalgm-mcp/server/http-context.js +44 -0
- package/runtime/scripts/amalgm-mcp/server/http-service.js +34 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +31 -465
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +57 -0
- package/runtime/scripts/amalgm-mcp/server/mcp-adapter.js +22 -0
- package/runtime/scripts/amalgm-mcp/server/mcp-http-service.js +24 -0
- package/runtime/scripts/amalgm-mcp/server/mcp-request-context.js +32 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +14 -73
- package/runtime/scripts/amalgm-mcp/server/routes/agents.js +29 -0
- package/runtime/scripts/amalgm-mcp/server/routes/apps.js +59 -0
- package/runtime/scripts/amalgm-mcp/server/routes/automations.js +45 -0
- package/runtime/scripts/amalgm-mcp/server/routes/browser.js +49 -0
- package/runtime/scripts/amalgm-mcp/server/routes/credentials.js +30 -0
- package/runtime/scripts/amalgm-mcp/server/routes/events.js +18 -0
- package/runtime/scripts/amalgm-mcp/server/routes/files.js +33 -0
- package/runtime/scripts/amalgm-mcp/server/routes/health.js +24 -0
- package/runtime/scripts/amalgm-mcp/server/routes/inbound.js +18 -0
- package/runtime/scripts/amalgm-mcp/server/routes/local.js +21 -0
- package/runtime/scripts/amalgm-mcp/server/routes/preferences.js +18 -0
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +23 -0
- package/runtime/scripts/amalgm-mcp/server/routes/toolbox.js +59 -0
- package/runtime/scripts/amalgm-mcp/server/routes/workspace.js +57 -0
- package/runtime/scripts/amalgm-mcp/server/service-lifecycle.js +54 -0
- package/runtime/scripts/amalgm-mcp/services/control-plane.js +19 -0
- package/runtime/scripts/amalgm-mcp/services/mcp-adapter.js +12 -0
- package/runtime/scripts/amalgm-mcp/services/runtime-worker.js +18 -0
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +21 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +3 -3
- package/runtime/scripts/amalgm-mcp/tests/agents-source-of-truth.test.js +82 -0
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +5 -5
- package/runtime/scripts/amalgm-mcp/tests/core-tools.test.js +37 -0
- package/runtime/scripts/amalgm-mcp/tests/mcp-surface.test.js +158 -0
- package/runtime/scripts/amalgm-mcp/tests/server-routing.test.js +81 -0
- package/runtime/scripts/amalgm-mcp/tests/server-services.test.js +156 -0
- package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +81 -0
- package/runtime/scripts/amalgm-mcp/tests/toolbox-service.test.js +250 -0
- package/runtime/scripts/amalgm-mcp/toolbox/mcp-surface.js +106 -0
- package/runtime/scripts/amalgm-mcp/toolbox/names.js +41 -0
- package/runtime/scripts/amalgm-mcp/toolbox/rest.js +107 -53
- package/runtime/scripts/amalgm-mcp/toolbox/runner.js +9 -75
- package/runtime/scripts/amalgm-mcp/toolbox/selection.js +22 -0
- package/runtime/scripts/amalgm-mcp/toolbox/service.js +227 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +9 -78
- package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +112 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +11 -74
- package/runtime/scripts/amalgm-mcp/workflows/compiler.js +1 -1
- package/runtime/scripts/amalgm-mcp/workflows/runner.js +4 -5
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
buildSystemToolboxCatalog,
|
|
8
|
+
mergeSystemToolboxCatalog,
|
|
9
|
+
persistedCatalogWithoutLegacyAggregate,
|
|
10
|
+
} = require('../toolbox/system-catalog');
|
|
11
|
+
|
|
12
|
+
test('system toolbox catalog exposes first-party default tools', () => {
|
|
13
|
+
const catalog = buildSystemToolboxCatalog();
|
|
14
|
+
|
|
15
|
+
assert.equal(catalog.tools['amalgm.browser'].name, 'Amalgm Browser');
|
|
16
|
+
assert.equal(catalog.tools['amalgm.browser'].origin, 'system');
|
|
17
|
+
assert.equal(catalog.tools['amalgm.browser'].policy.firstParty, true);
|
|
18
|
+
assert.equal(
|
|
19
|
+
catalog.toolActions['amalgm.browser.browser_navigate'].sourceAction.mcpToolName,
|
|
20
|
+
'toolbox__amalgm_browser_browser_navigate',
|
|
21
|
+
);
|
|
22
|
+
assert.equal(
|
|
23
|
+
catalog.toolActions['amalgm.browser.browser_navigate'].sourceAction.handlerName,
|
|
24
|
+
'browser_navigate',
|
|
25
|
+
);
|
|
26
|
+
assert.equal(
|
|
27
|
+
catalog.toolActions['amalgm.notifications.notify_user'].sourceAction.mcpToolName,
|
|
28
|
+
'toolbox__amalgm_notifications_notify_user',
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('system toolbox catalog merge preserves persisted records', () => {
|
|
33
|
+
const merged = mergeSystemToolboxCatalog({
|
|
34
|
+
version: 1,
|
|
35
|
+
tools: {
|
|
36
|
+
'amalgm.browser': {
|
|
37
|
+
id: 'amalgm.browser',
|
|
38
|
+
name: 'Persisted Browser',
|
|
39
|
+
origin: 'system',
|
|
40
|
+
},
|
|
41
|
+
'custom.cli': {
|
|
42
|
+
id: 'custom.cli',
|
|
43
|
+
name: 'Custom CLI',
|
|
44
|
+
origin: 'user',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
toolActions: {
|
|
48
|
+
'amalgm.browser.browser_navigate': {
|
|
49
|
+
id: 'amalgm.browser.browser_navigate',
|
|
50
|
+
toolId: 'amalgm.browser',
|
|
51
|
+
name: 'persisted_navigate',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.equal(merged.tools['amalgm.browser'].name, 'Persisted Browser');
|
|
57
|
+
assert.equal(merged.tools['custom.cli'].name, 'Custom CLI');
|
|
58
|
+
assert.equal(
|
|
59
|
+
merged.toolActions['amalgm.browser.browser_navigate'].name,
|
|
60
|
+
'persisted_navigate',
|
|
61
|
+
);
|
|
62
|
+
assert.equal(merged.toolActions['amalgm.apps.apps_list'].name, 'apps_list');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('system toolbox catalog omits legacy aggregate amalgm records', () => {
|
|
66
|
+
const filtered = persistedCatalogWithoutLegacyAggregate({
|
|
67
|
+
tools: {
|
|
68
|
+
amalgm: { id: 'amalgm', origin: 'system' },
|
|
69
|
+
'custom.cli': { id: 'custom.cli', origin: 'user' },
|
|
70
|
+
},
|
|
71
|
+
toolActions: {
|
|
72
|
+
'amalgm.notify_user': { id: 'amalgm.notify_user', toolId: 'amalgm' },
|
|
73
|
+
'custom.cli.run': { id: 'custom.cli.run', toolId: 'custom.cli' },
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.equal(filtered.tools.amalgm, undefined);
|
|
78
|
+
assert.equal(filtered.toolActions['amalgm.notify_user'], undefined);
|
|
79
|
+
assert.equal(filtered.tools['custom.cli'].id, 'custom.cli');
|
|
80
|
+
assert.equal(filtered.toolActions['custom.cli.run'].id, 'custom.cli.run');
|
|
81
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
|
|
6
|
+
const { createToolboxRest } = require('../toolbox/rest');
|
|
7
|
+
const { toolboxMcpToolName } = require('../toolbox/names');
|
|
8
|
+
const { createToolboxService } = require('../toolbox/service');
|
|
9
|
+
|
|
10
|
+
function sampleToolbox() {
|
|
11
|
+
return {
|
|
12
|
+
version: 1,
|
|
13
|
+
tools: {
|
|
14
|
+
'acme.tool': {
|
|
15
|
+
id: 'acme.tool',
|
|
16
|
+
name: 'Acme Tool',
|
|
17
|
+
type: 'cli',
|
|
18
|
+
owner: 'user',
|
|
19
|
+
origin: 'user',
|
|
20
|
+
status: 'enabled',
|
|
21
|
+
source: { command: 'acme' },
|
|
22
|
+
discovery: { mode: 'single' },
|
|
23
|
+
},
|
|
24
|
+
'api.weather': {
|
|
25
|
+
id: 'api.weather',
|
|
26
|
+
name: 'Weather API',
|
|
27
|
+
type: 'api',
|
|
28
|
+
owner: 'user',
|
|
29
|
+
origin: 'user',
|
|
30
|
+
status: 'enabled',
|
|
31
|
+
source: { baseUrl: 'https://weather.example.test' },
|
|
32
|
+
discovery: { mode: 'static' },
|
|
33
|
+
},
|
|
34
|
+
'remote.mcp': {
|
|
35
|
+
id: 'remote.mcp',
|
|
36
|
+
name: 'Remote MCP',
|
|
37
|
+
type: 'mcp',
|
|
38
|
+
owner: 'user',
|
|
39
|
+
origin: 'user',
|
|
40
|
+
status: 'enabled',
|
|
41
|
+
source: { transport: 'http', url: 'https://mcp.example.test' },
|
|
42
|
+
discovery: { mode: 'dynamic' },
|
|
43
|
+
},
|
|
44
|
+
amalgm: {
|
|
45
|
+
id: 'amalgm',
|
|
46
|
+
name: 'amalgm',
|
|
47
|
+
type: 'mcp',
|
|
48
|
+
owner: 'amalgm',
|
|
49
|
+
origin: 'system',
|
|
50
|
+
status: 'enabled',
|
|
51
|
+
source: { transport: 'http', url: 'http://127.0.0.1:8083/mcp' },
|
|
52
|
+
discovery: { mode: 'dynamic' },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
toolActions: {
|
|
56
|
+
'acme.tool.run': {
|
|
57
|
+
id: 'acme.tool.run',
|
|
58
|
+
toolId: 'acme.tool',
|
|
59
|
+
name: 'run',
|
|
60
|
+
description: 'Run Acme.',
|
|
61
|
+
status: 'enabled',
|
|
62
|
+
inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
|
|
63
|
+
},
|
|
64
|
+
'acme.tool.disabled': {
|
|
65
|
+
id: 'acme.tool.disabled',
|
|
66
|
+
toolId: 'acme.tool',
|
|
67
|
+
name: 'disabled',
|
|
68
|
+
status: 'disabled',
|
|
69
|
+
inputSchema: { type: 'object', properties: {} },
|
|
70
|
+
},
|
|
71
|
+
'api.weather.lookup': {
|
|
72
|
+
id: 'api.weather.lookup',
|
|
73
|
+
toolId: 'api.weather',
|
|
74
|
+
name: 'lookup',
|
|
75
|
+
description: 'Look up weather.',
|
|
76
|
+
status: 'enabled',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
enum: ['unsupported-top-level-key'],
|
|
80
|
+
oneOf: [{ required: ['city'] }],
|
|
81
|
+
properties: { city: { type: 'string' } },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
'remote.mcp.search': {
|
|
85
|
+
id: 'remote.mcp.search',
|
|
86
|
+
toolId: 'remote.mcp',
|
|
87
|
+
name: 'search',
|
|
88
|
+
status: 'enabled',
|
|
89
|
+
inputSchema: { type: 'object', properties: {} },
|
|
90
|
+
},
|
|
91
|
+
'amalgm.notify_user': {
|
|
92
|
+
id: 'amalgm.notify_user',
|
|
93
|
+
toolId: 'amalgm',
|
|
94
|
+
name: 'notify_user',
|
|
95
|
+
status: 'enabled',
|
|
96
|
+
inputSchema: { type: 'object', properties: {} },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createRepository(toolbox = sampleToolbox()) {
|
|
103
|
+
return {
|
|
104
|
+
deleteTool(id, options) {
|
|
105
|
+
return { ok: true, deleted: true, id, options };
|
|
106
|
+
},
|
|
107
|
+
deleteToolAction(id, options) {
|
|
108
|
+
return { ok: true, deleted: true, id, options };
|
|
109
|
+
},
|
|
110
|
+
getTool(id, options = {}) {
|
|
111
|
+
const tool = toolbox.tools[id];
|
|
112
|
+
if (!tool) throw new Error(`Unknown tool: ${id}`);
|
|
113
|
+
const actions = Object.values(toolbox.toolActions).filter((action) => action.toolId === id);
|
|
114
|
+
return { tool, ...(options.includeActions === false ? {} : { actions }) };
|
|
115
|
+
},
|
|
116
|
+
getToolAction(id) {
|
|
117
|
+
const action = toolbox.toolActions[id];
|
|
118
|
+
if (!action) throw new Error(`Unknown tool action: ${id}`);
|
|
119
|
+
return { action };
|
|
120
|
+
},
|
|
121
|
+
readToolbox() {
|
|
122
|
+
return toolbox;
|
|
123
|
+
},
|
|
124
|
+
updateTool(input) {
|
|
125
|
+
return { tool: input, actions: [] };
|
|
126
|
+
},
|
|
127
|
+
updateToolAction(input) {
|
|
128
|
+
return input;
|
|
129
|
+
},
|
|
130
|
+
upsertTool(input) {
|
|
131
|
+
return { tool: input, actions: input.actions || [] };
|
|
132
|
+
},
|
|
133
|
+
upsertToolAction(input) {
|
|
134
|
+
return input;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function captureJson() {
|
|
140
|
+
const calls = [];
|
|
141
|
+
return {
|
|
142
|
+
calls,
|
|
143
|
+
sendJson(status, payload) {
|
|
144
|
+
calls.push({ status, payload });
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
test('toolbox service lists compact registry views', () => {
|
|
150
|
+
const service = createToolboxService(createRepository());
|
|
151
|
+
const payload = service.listTools({ type: 'cli', include_actions: true });
|
|
152
|
+
|
|
153
|
+
assert.deepEqual(payload.tools.map((tool) => tool.id), ['acme.tool']);
|
|
154
|
+
assert.equal(payload.tools[0].actionCount, 2);
|
|
155
|
+
assert.equal(payload.tools[0].actions[0].ref, 'acme.tool.run');
|
|
156
|
+
assert.equal(payload.tools[0].actions[0].mcpToolName, toolboxMcpToolName({ id: 'acme.tool.run' }));
|
|
157
|
+
assert.equal(payload.toolActions['api.weather.lookup'].name, 'lookup');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('toolbox service overlays first-party system catalog entries', () => {
|
|
161
|
+
const service = createToolboxService(createRepository());
|
|
162
|
+
const catalog = service.readCatalog();
|
|
163
|
+
const browser = service.getTool('amalgm.browser', { includeActions: false });
|
|
164
|
+
const navigate = service.getAction('amalgm.browser.browser_navigate');
|
|
165
|
+
|
|
166
|
+
assert.equal(catalog.tools.amalgm, undefined);
|
|
167
|
+
assert.equal(catalog.toolActions['amalgm.notify_user'], undefined);
|
|
168
|
+
assert.equal(catalog.tools['amalgm.browser'].origin, 'system');
|
|
169
|
+
assert.equal(browser.tool.name, 'Amalgm Browser');
|
|
170
|
+
assert.equal(browser.actions, undefined);
|
|
171
|
+
assert.equal(navigate.action.sourceAction.mcpToolName, 'toolbox__amalgm_browser_browser_navigate');
|
|
172
|
+
assert.equal(service.listTools({ origin: 'system' }).tools.some((tool) => tool.id === 'amalgm.browser'), true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('toolbox service resolves only runnable CLI/API actions', () => {
|
|
176
|
+
const service = createToolboxService(createRepository());
|
|
177
|
+
|
|
178
|
+
assert.deepEqual(
|
|
179
|
+
service.listRunnableActions().map(({ action }) => action.id),
|
|
180
|
+
['acme.tool.run', 'api.weather.lookup'],
|
|
181
|
+
);
|
|
182
|
+
assert.equal(service.resolveRunnableAction('api.weather', 'api.weather.lookup')?.action.id, 'api.weather.lookup');
|
|
183
|
+
assert.equal(service.resolveRunnableAction('remote.mcp', 'search'), null);
|
|
184
|
+
assert.equal(service.resolveRunnableAction('amalgm', 'notify_user'), null);
|
|
185
|
+
|
|
186
|
+
const selected = service.listRunnableActions({
|
|
187
|
+
sessionMetadata: { tools: { mode: 'selected', selectedToolIds: ['api.weather.lookup'] } },
|
|
188
|
+
});
|
|
189
|
+
assert.deepEqual(selected.map(({ action }) => action.id), ['api.weather.lookup']);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('toolbox service derives only external MCP ids from selected tools', () => {
|
|
193
|
+
const service = createToolboxService(createRepository());
|
|
194
|
+
|
|
195
|
+
assert.deepEqual(service.externalMcpToolIdsForMode('all'), ['remote.mcp']);
|
|
196
|
+
assert.deepEqual(
|
|
197
|
+
service.externalMcpToolIdsForSelection([
|
|
198
|
+
'remote.mcp',
|
|
199
|
+
'amalgm.browser',
|
|
200
|
+
'api.weather',
|
|
201
|
+
'missing.tool',
|
|
202
|
+
]),
|
|
203
|
+
['remote.mcp'],
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('toolbox service exposes portable MCP descriptors', () => {
|
|
208
|
+
const service = createToolboxService(createRepository());
|
|
209
|
+
const descriptors = service.listMcpTools({
|
|
210
|
+
sessionMetadata: { tools: { mode: 'selected', selectedToolIds: ['api.weather.lookup'] } },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
assert.equal(descriptors.length, 1);
|
|
214
|
+
assert.equal(descriptors[0].name, toolboxMcpToolName({ id: 'api.weather.lookup' }));
|
|
215
|
+
assert.equal(descriptors[0].inputSchema.enum, undefined);
|
|
216
|
+
assert.equal(descriptors[0].inputSchema.oneOf, undefined);
|
|
217
|
+
assert.deepEqual(descriptors[0].inputSchema.properties, { city: { type: 'string' } });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('toolbox REST adapter delegates to service methods', async () => {
|
|
221
|
+
const service = createToolboxService(createRepository());
|
|
222
|
+
const rest = createToolboxRest(service);
|
|
223
|
+
const getTool = captureJson();
|
|
224
|
+
const updateAction = captureJson();
|
|
225
|
+
const missingDelete = captureJson();
|
|
226
|
+
|
|
227
|
+
await rest.handleGetTool({ id: 'acme.tool', include_actions: 'false' }, getTool.sendJson);
|
|
228
|
+
await rest.handleUpdateAction({
|
|
229
|
+
action: { id: 'acme.tool.run', description: 'New description' },
|
|
230
|
+
clientMutationId: 'mutation-1',
|
|
231
|
+
}, updateAction.sendJson);
|
|
232
|
+
await rest.handleDeleteTool({}, missingDelete.sendJson);
|
|
233
|
+
|
|
234
|
+
assert.deepEqual(getTool.calls, [{
|
|
235
|
+
status: 200,
|
|
236
|
+
payload: { tool: sampleToolbox().tools['acme.tool'] },
|
|
237
|
+
}]);
|
|
238
|
+
assert.deepEqual(updateAction.calls, [{
|
|
239
|
+
status: 200,
|
|
240
|
+
payload: {
|
|
241
|
+
ok: true,
|
|
242
|
+
action: {
|
|
243
|
+
id: 'acme.tool.run',
|
|
244
|
+
description: 'New description',
|
|
245
|
+
clientMutationId: 'mutation-1',
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
}]);
|
|
249
|
+
assert.deepEqual(missingDelete.calls, [{ status: 400, payload: { error: 'id is required' } }]);
|
|
250
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Toolbox MCP surface.
|
|
5
|
+
*
|
|
6
|
+
* This is the adapter-facing view of tools/actions. The MCP server should ask
|
|
7
|
+
* this module what tools exist and how to call them; it should not assemble
|
|
8
|
+
* built-ins, toolbox management tools, and user actions itself.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { errorResult } = require('../lib/tool-result');
|
|
12
|
+
const { CORE_TOOLS } = require('../server/core-tools');
|
|
13
|
+
const { toolboxMcpToolName } = require('./names');
|
|
14
|
+
const { callToolboxMcpTool } = require('./runner');
|
|
15
|
+
const {
|
|
16
|
+
selectedContainsToolOrAction,
|
|
17
|
+
selectedToolIdsForContext,
|
|
18
|
+
} = require('./selection');
|
|
19
|
+
const {
|
|
20
|
+
defaultToolboxService: toolboxService,
|
|
21
|
+
} = require('./service');
|
|
22
|
+
|
|
23
|
+
const DEFAULT_STATIC_MCP_TOOLS = [
|
|
24
|
+
...CORE_TOOLS,
|
|
25
|
+
...require('./tools'),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function toolDescriptor(tool) {
|
|
29
|
+
return {
|
|
30
|
+
name: mcpNameForStaticTool(tool),
|
|
31
|
+
description: tool.description,
|
|
32
|
+
inputSchema: tool.inputSchema,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mcpNameForStaticTool(tool) {
|
|
37
|
+
return tool.toolboxActionId
|
|
38
|
+
? toolboxMcpToolName({ id: tool.toolboxActionId })
|
|
39
|
+
: tool.name;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function defaultListDynamicTools(context) {
|
|
43
|
+
return toolboxService.listMcpTools(context);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isStaticToolSelected(tool, selectedToolIds) {
|
|
47
|
+
return selectedContainsToolOrAction(
|
|
48
|
+
selectedToolIds,
|
|
49
|
+
tool.toolboxToolId,
|
|
50
|
+
tool.toolboxActionId,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createToolboxMcpSurface(options = {}) {
|
|
55
|
+
const staticTools = Array.isArray(options.staticTools)
|
|
56
|
+
? options.staticTools
|
|
57
|
+
: DEFAULT_STATIC_MCP_TOOLS;
|
|
58
|
+
const listDynamicTools = options.listDynamicTools || defaultListDynamicTools;
|
|
59
|
+
const callDynamicTool = options.callDynamicTool || callToolboxMcpTool;
|
|
60
|
+
|
|
61
|
+
function staticToolsForContext(context) {
|
|
62
|
+
const selectedToolIds = selectedToolIdsForContext(context);
|
|
63
|
+
return staticTools.filter((tool) => isStaticToolSelected(tool, selectedToolIds));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function findStaticTool(toolName, context) {
|
|
67
|
+
return staticToolsForContext(context)
|
|
68
|
+
.find((tool) => mcpNameForStaticTool(tool) === toolName) || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function listTools(context) {
|
|
72
|
+
const dynamicTools = await listDynamicTools(context);
|
|
73
|
+
return [
|
|
74
|
+
...staticToolsForContext(context).map(toolDescriptor),
|
|
75
|
+
...(Array.isArray(dynamicTools) ? dynamicTools : []),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function callTool(toolName, toolArgs = {}, context) {
|
|
80
|
+
const staticTool = findStaticTool(toolName, context);
|
|
81
|
+
if (staticTool) {
|
|
82
|
+
try {
|
|
83
|
+
return await staticTool.handler(toolArgs, context);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return errorResult(`Error: ${error?.message || String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const dynamicResult = await callDynamicTool(toolName, toolArgs, context);
|
|
90
|
+
if (dynamicResult) return dynamicResult;
|
|
91
|
+
return errorResult(`Unknown tool: ${toolName}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
callTool,
|
|
96
|
+
findStaticTool,
|
|
97
|
+
listTools,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
DEFAULT_STATIC_MCP_TOOLS,
|
|
103
|
+
createToolboxMcpSurface,
|
|
104
|
+
mcpNameForStaticTool,
|
|
105
|
+
toolDescriptor,
|
|
106
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TOOLBOX_TOOL_PREFIX = 'toolbox__';
|
|
4
|
+
const MODEL_UNSUPPORTED_TOP_LEVEL_SCHEMA_KEYS = ['anyOf', 'oneOf', 'allOf', 'not', 'enum'];
|
|
5
|
+
|
|
6
|
+
function isObject(value) {
|
|
7
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function safeMcpName(value) {
|
|
11
|
+
return String(value || '')
|
|
12
|
+
.trim()
|
|
13
|
+
.replace(/[^A-Za-z0-9_.-]+/g, '_')
|
|
14
|
+
.replace(/[.-]+/g, '_')
|
|
15
|
+
.replace(/_+/g, '_')
|
|
16
|
+
.replace(/^_+|_+$/g, '')
|
|
17
|
+
|| 'tool';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toolboxMcpToolName(action) {
|
|
21
|
+
return `${TOOLBOX_TOOL_PREFIX}${safeMcpName(action.id)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function portableInputSchema(inputSchema) {
|
|
25
|
+
if (!isObject(inputSchema) || inputSchema.type !== 'object') {
|
|
26
|
+
return { type: 'object', properties: {}, additionalProperties: true };
|
|
27
|
+
}
|
|
28
|
+
const schema = { ...inputSchema };
|
|
29
|
+
for (const key of MODEL_UNSUPPORTED_TOP_LEVEL_SCHEMA_KEYS) {
|
|
30
|
+
delete schema[key];
|
|
31
|
+
}
|
|
32
|
+
if (!isObject(schema.properties)) schema.properties = {};
|
|
33
|
+
return schema;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
TOOLBOX_TOOL_PREFIX,
|
|
38
|
+
portableInputSchema,
|
|
39
|
+
safeMcpName,
|
|
40
|
+
toolboxMcpToolName,
|
|
41
|
+
};
|
|
@@ -7,69 +7,123 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
readToolbox,
|
|
13
|
-
upsertTool,
|
|
14
|
-
upsertToolAction,
|
|
15
|
-
} = require('./store');
|
|
10
|
+
defaultToolboxService,
|
|
11
|
+
} = require('./service');
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
function createToolboxRest(toolboxService = defaultToolboxService) {
|
|
14
|
+
async function handleList(sendJson) {
|
|
15
|
+
sendJson(200, toolboxService.readCatalog());
|
|
16
|
+
}
|
|
20
17
|
|
|
21
|
-
async function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
const saved = upsertTool(tool);
|
|
30
|
-
sendJson(200, { ok: true, ...saved });
|
|
31
|
-
} catch (error) {
|
|
32
|
-
sendJson(400, { error: error instanceof Error ? error.message : 'Failed to save tool' });
|
|
18
|
+
async function handleGetTool(query, sendJson) {
|
|
19
|
+
try {
|
|
20
|
+
const id = query?.id || query?.toolId;
|
|
21
|
+
const includeActions = query?.include_actions !== 'false' && query?.includeActions !== 'false';
|
|
22
|
+
sendJson(200, toolboxService.getTool(id, { includeActions }));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
sendJson(400, { error: error instanceof Error ? error.message : 'Failed to read tool' });
|
|
25
|
+
}
|
|
33
26
|
}
|
|
34
|
-
}
|
|
35
27
|
|
|
36
|
-
async function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
async function handleUpsertTool(body, sendJson) {
|
|
29
|
+
try {
|
|
30
|
+
const rawTool = body?.tool && typeof body.tool === 'object' ? body.tool : body;
|
|
31
|
+
const tool = {
|
|
32
|
+
...rawTool,
|
|
33
|
+
actions: body?.actions ?? rawTool?.actions,
|
|
34
|
+
clientMutationId: body?.clientMutationId ?? rawTool?.clientMutationId,
|
|
35
|
+
};
|
|
36
|
+
const saved = toolboxService.registerTool(tool);
|
|
37
|
+
sendJson(200, { ok: true, ...saved });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
sendJson(400, { error: error instanceof Error ? error.message : 'Failed to save tool' });
|
|
40
|
+
}
|
|
43
41
|
}
|
|
44
|
-
}
|
|
45
42
|
|
|
46
|
-
async function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
async function handleUpdateTool(body, sendJson) {
|
|
44
|
+
try {
|
|
45
|
+
const rawTool = body?.tool && typeof body.tool === 'object' ? body.tool : body;
|
|
46
|
+
const saved = toolboxService.updateTool({
|
|
47
|
+
...rawTool,
|
|
48
|
+
clientMutationId: body?.clientMutationId ?? rawTool?.clientMutationId,
|
|
49
|
+
});
|
|
50
|
+
sendJson(200, { ok: true, ...saved });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
sendJson(400, { error: error instanceof Error ? error.message : 'Failed to update tool' });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function handleDeleteTool(body, sendJson) {
|
|
57
|
+
try {
|
|
58
|
+
const id = body?.id || body?.toolId;
|
|
59
|
+
if (!id) return sendJson(400, { error: 'id is required' });
|
|
60
|
+
sendJson(200, toolboxService.deleteTool(id, { clientMutationId: body?.clientMutationId }));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
sendJson(400, { error: error instanceof Error ? error.message : 'Failed to delete tool' });
|
|
63
|
+
}
|
|
56
64
|
}
|
|
57
|
-
}
|
|
58
65
|
|
|
59
|
-
async function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
async function handleGetAction(query, sendJson) {
|
|
67
|
+
try {
|
|
68
|
+
const id = query?.id || query?.actionId;
|
|
69
|
+
sendJson(200, toolboxService.getAction(id));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
sendJson(400, { error: error instanceof Error ? error.message : 'Failed to read tool action' });
|
|
72
|
+
}
|
|
66
73
|
}
|
|
74
|
+
|
|
75
|
+
async function handleUpsertAction(body, sendJson) {
|
|
76
|
+
try {
|
|
77
|
+
const rawAction = body?.action && typeof body.action === 'object' ? body.action : body;
|
|
78
|
+
const action = toolboxService.registerAction({
|
|
79
|
+
...rawAction,
|
|
80
|
+
clientMutationId: body?.clientMutationId ?? rawAction?.clientMutationId,
|
|
81
|
+
});
|
|
82
|
+
sendJson(200, { ok: true, action });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
sendJson(400, { error: error instanceof Error ? error.message : 'Failed to save tool action' });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function handleUpdateAction(body, sendJson) {
|
|
89
|
+
try {
|
|
90
|
+
const rawAction = body?.action && typeof body.action === 'object' ? body.action : body;
|
|
91
|
+
const action = toolboxService.updateAction({
|
|
92
|
+
...rawAction,
|
|
93
|
+
clientMutationId: body?.clientMutationId ?? rawAction?.clientMutationId,
|
|
94
|
+
});
|
|
95
|
+
sendJson(200, { ok: true, action });
|
|
96
|
+
} catch (error) {
|
|
97
|
+
sendJson(400, { error: error instanceof Error ? error.message : 'Failed to update tool action' });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function handleDeleteAction(body, sendJson) {
|
|
102
|
+
try {
|
|
103
|
+
const id = body?.id || body?.actionId;
|
|
104
|
+
if (!id) return sendJson(400, { error: 'id is required' });
|
|
105
|
+
sendJson(200, toolboxService.deleteAction(id, { clientMutationId: body?.clientMutationId }));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
sendJson(400, { error: error instanceof Error ? error.message : 'Failed to delete tool action' });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
handleDeleteAction,
|
|
113
|
+
handleDeleteTool,
|
|
114
|
+
handleGetAction,
|
|
115
|
+
handleGetTool,
|
|
116
|
+
handleList,
|
|
117
|
+
handleUpdateAction,
|
|
118
|
+
handleUpdateTool,
|
|
119
|
+
handleUpsertAction,
|
|
120
|
+
handleUpsertTool,
|
|
121
|
+
};
|
|
67
122
|
}
|
|
68
123
|
|
|
124
|
+
const defaultRest = createToolboxRest();
|
|
125
|
+
|
|
69
126
|
module.exports = {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
handleList,
|
|
73
|
-
handleUpsertAction,
|
|
74
|
-
handleUpsertTool,
|
|
127
|
+
createToolboxRest,
|
|
128
|
+
...defaultRest,
|
|
75
129
|
};
|