@amalgm/tools 0.1.7-main.34436367218 → 0.1.7-main.34938545892
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/PURPOSE.md +33 -1
- package/README.md +35 -5
- package/dist/api-driver.d.ts +2 -1
- package/dist/api-driver.js +67 -48
- package/dist/cli-driver.d.ts +4 -3
- package/dist/cli-driver.js +43 -40
- package/dist/cli.d.ts +7 -2
- package/dist/cli.js +51 -49
- package/dist/commands.d.ts +27 -0
- package/dist/commands.js +16 -0
- package/dist/connection-auth.d.ts +11 -0
- package/dist/connection-auth.js +98 -0
- package/dist/connection-source.d.ts +23 -0
- package/dist/connection-source.js +62 -0
- package/dist/connection-types.d.ts +34 -0
- package/dist/connection-types.js +1 -0
- package/dist/connections.d.ts +14 -0
- package/dist/connections.js +98 -0
- package/dist/credential-operations.d.ts +7 -0
- package/dist/credential-operations.js +37 -0
- package/dist/definition.js +11 -3
- package/dist/host-mcp-tool.d.ts +18 -1
- package/dist/http-mutations.d.ts +63 -0
- package/dist/http-mutations.js +41 -0
- package/dist/http.d.ts +2 -3
- package/dist/http.js +9 -3
- package/dist/ids.d.ts +3 -1
- package/dist/ids.js +7 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +4 -0
- package/dist/input.js +7 -32
- package/dist/loadout.d.ts +3 -0
- package/dist/loadout.js +23 -0
- package/dist/mcp-driver.d.ts +16 -0
- package/dist/mcp-driver.js +44 -0
- package/dist/mcp-search.d.ts +8 -0
- package/dist/mcp-search.js +51 -0
- package/dist/mcp-server.d.ts +4 -3
- package/dist/mcp-types.d.ts +13 -0
- package/dist/mcp-types.js +1 -0
- package/dist/mcp.d.ts +6 -5
- package/dist/mcp.js +52 -26
- package/dist/oauth.d.ts +29 -0
- package/dist/oauth.js +131 -0
- package/dist/prepared-catalog.d.ts +5 -0
- package/dist/prepared-catalog.js +44 -0
- package/dist/results.d.ts +4 -1
- package/dist/results.js +21 -16
- package/dist/selection.js +3 -0
- package/dist/toolbox-state-dir.d.ts +2 -0
- package/dist/toolbox-state-dir.js +18 -0
- package/dist/toolbox-view.d.ts +4 -1
- package/dist/toolbox-view.js +8 -2
- package/dist/toolbox.d.ts +15 -2
- package/dist/toolbox.js +36 -30
- package/dist/types.d.ts +33 -37
- package/dist/updates.d.ts +7 -2
- package/dist/updates.js +15 -1
- package/docs/SHELL_INTEGRATION.md +8 -3
- package/package.json +5 -4
- package/skills/tools/SKILL.md +6 -0
package/PURPOSE.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
Amalgm Tools lets a user define, keep, select, and run agent capabilities as a
|
|
6
6
|
standalone product or inside Amalgm. It serves people building agents,
|
|
7
7
|
automations, and applications that need one small local source of truth for
|
|
8
|
-
CLI, HTTP API, and MCP tools. When the product is healthy, every callable
|
|
8
|
+
CLI, HTTP API, GraphQL, and MCP tools. When the product is healthy, every callable
|
|
9
9
|
action has one stable identity and one owning tool, a selected loadout exposes
|
|
10
10
|
only its granted capabilities, and the SDK, CLI, and MCP surfaces all observe
|
|
11
11
|
and execute the same catalog.
|
|
@@ -30,6 +30,10 @@ and parity evidence only.
|
|
|
30
30
|
- A **loadout** is a set of tool or action ids used to derive a read-only view
|
|
31
31
|
of the Toolbox.
|
|
32
32
|
- A **driver** executes actions for one tool type.
|
|
33
|
+
- A **connection** is a tool's current admission to execution, with credential
|
|
34
|
+
references resolved by the host's encrypted vault; it is not another catalog.
|
|
35
|
+
- **Exposure** is an agent loadout's presentation of a granted action: direct,
|
|
36
|
+
searchable, or hidden. An action override is more specific than its tool default.
|
|
33
37
|
- An **MCP server** is a separately identified runtime endpoint that may expose
|
|
34
38
|
one or more tools; selecting a tool never changes that server's identity.
|
|
35
39
|
- A **system tool** is an embedder-owned definition projected into the catalog;
|
|
@@ -133,6 +137,34 @@ locked dependency graph on the supported Node toolchain.
|
|
|
133
137
|
when a first-party release currently maps them one-to-one.
|
|
134
138
|
26. A scoped Toolbox MCP server advertises and calls only the selected loadout;
|
|
135
139
|
management actions are included only when the composing host asks for them.
|
|
140
|
+
27. Preparing an exact catalog revision captures a read-only loadout whose
|
|
141
|
+
definitions and scope cannot change. Later catalog edits affect future
|
|
142
|
+
preparation; already prepared calls use their captured definitions and the
|
|
143
|
+
host's current credential and execution effects.
|
|
144
|
+
28. Disconnect disables the parent tool and removes its saved authentication;
|
|
145
|
+
it retains the definition and action schemas for reconnection. Current parent
|
|
146
|
+
admission gates every invocation, including an already prepared execution.
|
|
147
|
+
A loadout edit never connects, disconnects, or changes the parent Toolbox.
|
|
148
|
+
29. A loadout grants only its selected tool/action ids. Omitted exposure means
|
|
149
|
+
direct; an explicit action exposure overrides its selected tool's exposure.
|
|
150
|
+
Hidden actions appear in neither listing, search, prompt descriptions, nor
|
|
151
|
+
invocation. Search exposes only searchable descriptors and its scoped call
|
|
152
|
+
action. An exposure entry cannot grant an unselected capability.
|
|
153
|
+
30. Every tool type crosses the same scoped MCP action boundary. ACP describes
|
|
154
|
+
the server supplied to an agent; MCP describes action schemas and results.
|
|
155
|
+
Native adapters do not discover tools or decide exposure. CLI descriptions
|
|
156
|
+
may inform system instructions, but never substitute for execution admission.
|
|
157
|
+
31. Connecting an MCP source discovers and stores its complete official action
|
|
158
|
+
schemas before activation. Invocation uses those captured schemas and exact
|
|
159
|
+
target names; a later remote catalog cannot expand an agent's grant.
|
|
160
|
+
32. OAuth access, refresh, and client secrets remain in the host vault.
|
|
161
|
+
Header, environment, and query authentication use source-bound vault references. Refresh
|
|
162
|
+
is serialized per credential, preserves rotated tokens, and cannot restore a
|
|
163
|
+
disconnected credential. Credential references are bound to the registered
|
|
164
|
+
source, so editing an endpoint cannot send an earlier secret to a new authority.
|
|
165
|
+
33. GraphQL actions store one operation document and send validated input as its
|
|
166
|
+
variables to the registered endpoint. API and GraphQL redirects never forward
|
|
167
|
+
credentials to another endpoint; callers cannot override registered targets.
|
|
136
168
|
|
|
137
169
|
## Predictable behavior
|
|
138
170
|
|
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
|
|
|
@@ -83,10 +89,14 @@ const server = createToolboxHttpServer({ toolbox });
|
|
|
83
89
|
await server.listen(8083);
|
|
84
90
|
```
|
|
85
91
|
|
|
86
|
-
The
|
|
92
|
+
The adapter serves `GET /toolbox`,
|
|
87
93
|
`GET /toolbox/tools?id=...`, and `GET /toolbox/actions?id=...` routes over
|
|
88
|
-
the same live Toolbox used by CLI and MCP consumers.
|
|
89
|
-
|
|
94
|
+
the same live Toolbox used by CLI and MCP consumers. `POST /toolbox/tools`
|
|
95
|
+
connects a definition with optional write-only credentials. `PATCH /toolbox/tools`
|
|
96
|
+
with `{ id, status: "disabled" }` disconnects it. `DELETE /toolbox/tools` removes
|
|
97
|
+
it. `POST /toolbox/actions/call` invokes `{ id, input }`.
|
|
98
|
+
The authenticated host selects the owner. OAuth authorization and callback
|
|
99
|
+
use `POST /toolbox/oauth/authorize` and `POST /toolbox/oauth/callback`.
|
|
90
100
|
|
|
91
101
|
## Notifications
|
|
92
102
|
|
|
@@ -102,11 +112,31 @@ and must not be presented as a second Channels implementation.
|
|
|
102
112
|
|
|
103
113
|
- `cli`: executed directly with `spawn`; a shell is never involved.
|
|
104
114
|
- `api`: same-origin HTTP actions with redirects refused.
|
|
115
|
+
- `graphql`: a saved operation document with validated input as its variables.
|
|
105
116
|
- `mcp`: a connection plus action projection. The composing MCP host supplies
|
|
106
117
|
the one MCP driver that owns discovery, sessions, and calls.
|
|
107
118
|
|
|
108
|
-
Secret values are never part of definitions. `secretEnv
|
|
109
|
-
contain opaque references resolved
|
|
119
|
+
Secret values are never part of definitions. `secretEnv`, `secretHeaders`,
|
|
120
|
+
and `secretQuery` contain opaque references resolved at invocation. A host
|
|
121
|
+
supplies `credentials: { get, set, delete }` backed by its encrypted vault.
|
|
122
|
+
`toolbox.connect({ definition, credentials })` stores authentication separately
|
|
123
|
+
and discovers MCP actions before activation. `toolbox.disconnect(id)` removes
|
|
124
|
+
authentication and disables execution while retaining the definition and actions.
|
|
125
|
+
|
|
126
|
+
A loadout contains `toolIds` (whole-tool or action ids) and optional `exposure`:
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const selected = toolbox.prepare(toolbox.catalog().revisionId, {
|
|
130
|
+
toolIds: ['service'],
|
|
131
|
+
exposure: { service: 'search', 'service.read': 'direct', 'service.erase': 'hidden' },
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Direct actions are listed normally. Searchable actions are reachable only through
|
|
136
|
+
`toolbox_search` and its scoped `toolbox_call`. Hidden actions cannot be listed,
|
|
137
|
+
searched, or invoked. Every type uses this same MCP surface; ACP supplies the
|
|
138
|
+
server to each agent adapter. A prepared view keeps its captured definitions
|
|
139
|
+
but still honors a subsequent parent disconnect.
|
|
110
140
|
See [PURPOSE.md](./PURPOSE.md) for the complete contract and
|
|
111
141
|
[SHELL_INTEGRATION.md](./docs/SHELL_INTEGRATION.md) for active composition.
|
|
112
142
|
The [Engine integration](./docs/ENGINE_INTEGRATION.md) page is historical only.
|
package/dist/api-driver.d.ts
CHANGED
package/dist/api-driver.js
CHANGED
|
@@ -26,13 +26,20 @@ async function readBody(response, maximum) {
|
|
|
26
26
|
chunks.push(value.subarray(0, room));
|
|
27
27
|
size += Math.min(room, value.length);
|
|
28
28
|
}
|
|
29
|
+
if (truncated) {
|
|
30
|
+
await reader.cancel();
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
29
33
|
}
|
|
30
34
|
return { text: Buffer.concat(chunks).toString('utf8'), truncated };
|
|
31
35
|
}
|
|
32
36
|
function timeoutSignal(timeoutMs, parent) {
|
|
33
37
|
const controller = new AbortController();
|
|
34
38
|
const abort = () => controller.abort(parent?.reason);
|
|
35
|
-
parent?.
|
|
39
|
+
if (parent?.aborted)
|
|
40
|
+
abort();
|
|
41
|
+
else
|
|
42
|
+
parent?.addEventListener('abort', abort, { once: true });
|
|
36
43
|
const timer = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
|
|
37
44
|
timer.unref();
|
|
38
45
|
return {
|
|
@@ -40,53 +47,65 @@ function timeoutSignal(timeoutMs, parent) {
|
|
|
40
47
|
close() { clearTimeout(timer); parent?.removeEventListener('abort', abort); },
|
|
41
48
|
};
|
|
42
49
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
async function callHttp({ tool, action, input, options }) {
|
|
51
|
+
const source = tool.source;
|
|
52
|
+
if (source.type !== 'api' && source.type !== 'graphql')
|
|
53
|
+
throw new Error('HTTP tool source required');
|
|
54
|
+
const target = action.target;
|
|
55
|
+
const graphql = action.target;
|
|
56
|
+
const base = new URL(source.type === 'api' ? source.baseUrl : source.url);
|
|
57
|
+
const url = source.type === 'api' ? new URL(fillPath(target.path, input), base) : base;
|
|
58
|
+
if (url.origin !== base.origin)
|
|
59
|
+
throw new Error(`API action ${action.id} leaves its registered origin`);
|
|
60
|
+
const query = source.type === 'api' ? input.query : undefined;
|
|
61
|
+
if (query && (typeof query !== 'object' || Array.isArray(query)))
|
|
62
|
+
throw new Error('input.query must be an object');
|
|
63
|
+
for (const [key, value] of Object.entries((query || {}))) {
|
|
64
|
+
if (value !== undefined && value !== null)
|
|
65
|
+
url.searchParams.append(key, String(value));
|
|
66
|
+
}
|
|
67
|
+
const secretQuery = await resolveReferences(source.secretQuery, options.resolveSecret);
|
|
68
|
+
for (const [key, value] of Object.entries(secretQuery))
|
|
69
|
+
url.searchParams.set(key, value);
|
|
70
|
+
const headers = await resolveReferences(source.secretHeaders, options.resolveSecret);
|
|
71
|
+
const method = source.type === 'api' ? target.method.toUpperCase() : 'POST';
|
|
72
|
+
const requestBody = source.type === 'api' ? (input.body ?? input) : {
|
|
73
|
+
query: graphql.document, variables: input,
|
|
74
|
+
...(graphql.operationName ? { operationName: graphql.operationName } : {}),
|
|
75
|
+
};
|
|
76
|
+
const timed = timeoutSignal(source.timeoutMs, options.signal);
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(url, {
|
|
79
|
+
method, redirect: 'manual', signal: timed.signal,
|
|
80
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
81
|
+
body: ['GET', 'HEAD'].includes(method) ? undefined : JSON.stringify(requestBody),
|
|
82
|
+
});
|
|
83
|
+
if (response.status >= 300 && response.status < 400)
|
|
84
|
+
return errorResult(`API tool ${action.id} refused redirect`);
|
|
85
|
+
const body = await readBody(response, source.maxOutputBytes);
|
|
86
|
+
if (!response.ok)
|
|
87
|
+
return errorResult(`HTTP ${response.status} ${response.statusText}\n\n${body.text}`);
|
|
88
|
+
let result;
|
|
62
89
|
try {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
if (response.status >= 300 && response.status < 400)
|
|
69
|
-
return errorResult(`API tool ${action.id} refused redirect`);
|
|
70
|
-
const body = await readBody(response, source.maxOutputBytes);
|
|
71
|
-
if (!response.ok)
|
|
72
|
-
return errorResult(`HTTP ${response.status} ${response.statusText}\n\n${body.text}`);
|
|
73
|
-
let result;
|
|
74
|
-
try {
|
|
75
|
-
result = structuredResult(JSON.parse(body.text));
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
result = textResult(body.text);
|
|
79
|
-
}
|
|
80
|
-
return { ...boundedResult(result, source.maxOutputBytes), ...(body.truncated ? { truncated: true } : {}) };
|
|
90
|
+
const value = JSON.parse(body.text);
|
|
91
|
+
result = structuredResult(value);
|
|
92
|
+
if (source.type === 'graphql' && Array.isArray(value?.errors) && value.errors.length)
|
|
93
|
+
result.isError = true;
|
|
81
94
|
}
|
|
82
|
-
catch
|
|
83
|
-
|
|
84
|
-
return errorResult(options.signal?.aborted ? `Tool ${action.id} was cancelled` : `Tool ${action.id} timed out`);
|
|
85
|
-
throw error;
|
|
95
|
+
catch {
|
|
96
|
+
result = textResult(body.text);
|
|
86
97
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
};
|
|
92
|
-
|
|
98
|
+
return { ...boundedResult(result, source.maxOutputBytes), ...(body.truncated ? { truncated: true } : {}) };
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (timed.signal.aborted)
|
|
102
|
+
return errorResult(options.signal?.aborted ? `Tool ${action.id} was cancelled` : `Tool ${action.id} timed out`);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
timed.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const apiDriver = { type: 'api', call: callHttp };
|
|
110
|
+
const graphqlDriver = { type: 'graphql', call: callHttp };
|
|
111
|
+
export { apiDriver, graphqlDriver };
|
package/dist/cli-driver.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { ToolDriver } from './types.js';
|
|
2
|
-
|
|
3
|
-
|
|
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 };
|
package/dist/cli-driver.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runCommand } from './process.js';
|
|
2
|
+
import { getDefaultEnvironment } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
2
3
|
import { errorResult, structuredResult, textResult } from './results.js';
|
|
3
4
|
import { resolveReferences } from './secrets.js';
|
|
4
5
|
function inputArguments(input) {
|
|
@@ -8,45 +9,47 @@ function inputArguments(input) {
|
|
|
8
9
|
}
|
|
9
10
|
return args;
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let result;
|
|
39
|
-
if (source.outputMode === 'json' && stdout) {
|
|
40
|
-
try {
|
|
41
|
-
result = structuredResult(JSON.parse(stdout));
|
|
12
|
+
function createCliDriver(environment = () => ({})) {
|
|
13
|
+
return {
|
|
14
|
+
type: 'cli',
|
|
15
|
+
async call({ tool, action, input, options }) {
|
|
16
|
+
const source = tool.source;
|
|
17
|
+
const target = action.target;
|
|
18
|
+
const secrets = await resolveReferences(source.secretEnv, options.resolveSecret);
|
|
19
|
+
const args = [...(source.args || []), ...(target.args || [])];
|
|
20
|
+
if (source.inputMode === 'argv')
|
|
21
|
+
args.push(...inputArguments(input));
|
|
22
|
+
const output = await runCommand({
|
|
23
|
+
command: source.command, args, cwd: source.cwd,
|
|
24
|
+
env: { ...getDefaultEnvironment(), ...(await environment(tool)), ...secrets },
|
|
25
|
+
stdin: source.inputMode === 'json-stdin' ? `${JSON.stringify(input)}\n` : undefined,
|
|
26
|
+
timeoutMs: source.timeoutMs, maximumBytes: source.maxOutputBytes, signal: options.signal,
|
|
27
|
+
});
|
|
28
|
+
const stdout = output.stdout.trim();
|
|
29
|
+
const stderr = output.stderr.trim();
|
|
30
|
+
if (output.timedOut)
|
|
31
|
+
return errorResult(`Tool ${action.id} timed out after ${source.timeoutMs}ms`);
|
|
32
|
+
if (output.aborted)
|
|
33
|
+
return errorResult(`Tool ${action.id} was cancelled`);
|
|
34
|
+
if (output.code !== 0) {
|
|
35
|
+
return errorResult([
|
|
36
|
+
`Tool ${action.id} exited with ${output.code ?? output.signal ?? 'unknown'}.`,
|
|
37
|
+
stdout && `stdout:\n${stdout}`, stderr && `stderr:\n${stderr}`,
|
|
38
|
+
].filter(Boolean).join('\n\n'));
|
|
42
39
|
}
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
let result;
|
|
41
|
+
if (source.outputMode === 'json' && stdout) {
|
|
42
|
+
try {
|
|
43
|
+
result = structuredResult(JSON.parse(stdout));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return errorResult(`Tool ${action.id} returned invalid JSON`);
|
|
47
|
+
}
|
|
45
48
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
result
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
export {
|
|
49
|
+
else
|
|
50
|
+
result = textResult(stdout || `Tool ${action.id} completed.`);
|
|
51
|
+
return { ...result, ...(output.truncated ? { truncated: true } : {}) };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
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
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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 };
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isActionMcpName } from './ids.js';
|
|
2
|
+
import { callMcpTool } from './mcp.js';
|
|
3
|
+
import { structuredValue } from './results.js';
|
|
4
|
+
function unwrap(name, result) {
|
|
5
|
+
if (result.isError)
|
|
6
|
+
throw new Error(result.content.filter((item) => item.type === 'text').map((item) => item.text).join('\n'));
|
|
7
|
+
return isActionMcpName(name) ? result : structuredValue(result.structuredContent);
|
|
8
|
+
}
|
|
9
|
+
function createToolboxCommands(toolbox, loadout) {
|
|
10
|
+
return {
|
|
11
|
+
async execute(name, args) {
|
|
12
|
+
return unwrap(name, await callMcpTool(toolbox, name, args, { loadout }));
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export { createToolboxCommands };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolCredential, ToolCredentialStore } from './connection-types.js';
|
|
2
|
+
import type { SecretResolver } from './types.js';
|
|
3
|
+
export declare function validateCredential(value: ToolCredential): ToolCredential;
|
|
4
|
+
/** Token refresh belongs to the connection; encryption belongs to the host. */
|
|
5
|
+
export declare class ToolAuthentication {
|
|
6
|
+
#private;
|
|
7
|
+
readonly store?: ToolCredentialStore | undefined;
|
|
8
|
+
private readonly fallback?;
|
|
9
|
+
constructor(store?: ToolCredentialStore | undefined, fallback?: SecretResolver | undefined);
|
|
10
|
+
resolve: (reference: string) => Promise<string | undefined>;
|
|
11
|
+
}
|