toolcraft 0.0.2 → 0.0.3
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 +458 -58
- package/dist/cli.compile-check.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +768 -40
- package/dist/human-in-loop/approval-tasks.d.ts +31 -0
- package/dist/human-in-loop/approval-tasks.js +201 -0
- package/dist/human-in-loop/approvals-commands.d.ts +11 -0
- package/dist/human-in-loop/approvals-commands.js +191 -0
- package/dist/human-in-loop/config.d.ts +11 -0
- package/dist/human-in-loop/config.js +21 -0
- package/dist/human-in-loop/default-provider.d.ts +2 -0
- package/dist/human-in-loop/default-provider.js +26 -0
- package/dist/human-in-loop/gate.d.ts +4 -0
- package/dist/human-in-loop/gate.js +57 -0
- package/dist/human-in-loop/index.d.ts +7 -0
- package/dist/human-in-loop/index.js +4 -0
- package/dist/human-in-loop/runner.d.ts +3 -0
- package/dist/human-in-loop/runner.js +196 -0
- package/dist/human-in-loop/spawn.d.ts +3 -0
- package/dist/human-in-loop/spawn.js +16 -0
- package/dist/human-in-loop/state-machine.d.ts +4 -0
- package/dist/human-in-loop/state-machine.js +10 -0
- package/dist/human-in-loop/types.d.ts +41 -0
- package/dist/human-in-loop/types.js +13 -0
- package/dist/index.compile-check.js +24 -0
- package/dist/index.d.ts +32 -13
- package/dist/index.js +82 -17
- package/dist/json-schema-converter.d.ts +21 -0
- package/dist/json-schema-converter.js +432 -0
- package/dist/mcp-proxy.d.ts +8 -0
- package/dist/mcp-proxy.js +383 -0
- package/dist/mcp.compile-check.js +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +103 -11
- package/dist/sdk.compile-check.js +77 -0
- package/dist/sdk.d.ts +14 -5
- package/dist/sdk.js +57 -6
- package/dist/user-error.d.ts +3 -0
- package/dist/user-error.js +6 -0
- package/node_modules/@poe-code/agent-human-in-loop/README.md +42 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/index.d.ts +5 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/index.js +3 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.d.ts +2 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.js +11 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.d.ts +4 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +40 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.d.ts +6 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +33 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.d.ts +4 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.js +4 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/types.d.ts +14 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/types.js +1 -0
- package/node_modules/@poe-code/agent-human-in-loop/package.json +25 -0
- package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +6 -0
- package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +175 -0
- package/node_modules/@poe-code/agent-mcp-config/dist/configs.d.ts +22 -0
- package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +74 -0
- package/node_modules/@poe-code/agent-mcp-config/dist/index.d.ts +3 -0
- package/node_modules/@poe-code/agent-mcp-config/dist/index.js +2 -0
- package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +31 -0
- package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +87 -0
- package/node_modules/@poe-code/agent-mcp-config/dist/types.d.ts +25 -0
- package/node_modules/@poe-code/agent-mcp-config/dist/types.js +1 -0
- package/node_modules/@poe-code/agent-mcp-config/package.json +25 -0
- package/node_modules/@poe-code/task-list/README.md +114 -0
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +466 -0
- package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +8 -0
- package/node_modules/@poe-code/task-list/dist/backends/utils.js +58 -0
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +444 -0
- package/node_modules/@poe-code/task-list/dist/index.d.ts +4 -0
- package/node_modules/@poe-code/task-list/dist/index.js +4 -0
- package/node_modules/@poe-code/task-list/dist/open.d.ts +3 -0
- package/node_modules/@poe-code/task-list/dist/open.js +34 -0
- package/node_modules/@poe-code/task-list/dist/schema/store.schema.json +32 -0
- package/node_modules/@poe-code/task-list/dist/schema/task.schema.json +33 -0
- package/node_modules/@poe-code/task-list/dist/state-machine.d.ts +16 -0
- package/node_modules/@poe-code/task-list/dist/state-machine.js +67 -0
- package/node_modules/@poe-code/task-list/dist/state.d.ts +29 -0
- package/node_modules/@poe-code/task-list/dist/state.js +61 -0
- package/node_modules/@poe-code/task-list/dist/types.d.ts +116 -0
- package/node_modules/@poe-code/task-list/dist/types.js +37 -0
- package/node_modules/@poe-code/task-list/package.json +26 -0
- package/package.json +22 -7
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createLogger } from "@poe-code/design-system";
|
|
5
|
+
import { HttpTransport, McpClient, StdioTransport } from "tiny-mcp-client";
|
|
6
|
+
import { convertJsonSchema } from "./json-schema-converter.js";
|
|
7
|
+
const GROUP_CONFIG_SYMBOL_DESCRIPTION = "toolcraft.group.config";
|
|
8
|
+
const MCP_PROXY_SCHEMA_URL = "https://poe-platform.github.io/poe-code/schemas/toolcraft/mcp-proxy.schema.json";
|
|
9
|
+
const DEFAULT_CLIENT_INFO = {
|
|
10
|
+
name: "toolcraft",
|
|
11
|
+
version: "0.0.1",
|
|
12
|
+
};
|
|
13
|
+
const proxyNodeSymbol = Symbol("toolcraft.mcpProxyNode");
|
|
14
|
+
const proxyConnectionSymbol = Symbol("toolcraft.mcpProxyConnection");
|
|
15
|
+
const shutdownDisposers = new Set();
|
|
16
|
+
function getInternalGroupConfig(group) {
|
|
17
|
+
const symbol = Object.getOwnPropertySymbols(group).find((candidate) => candidate.description === GROUP_CONFIG_SYMBOL_DESCRIPTION);
|
|
18
|
+
if (symbol === undefined) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
return (group[symbol] ??
|
|
22
|
+
{});
|
|
23
|
+
}
|
|
24
|
+
function isProxyNode(node) {
|
|
25
|
+
return node[proxyNodeSymbol] === true;
|
|
26
|
+
}
|
|
27
|
+
function markProxyNode(node) {
|
|
28
|
+
Object.defineProperty(node, proxyNodeSymbol, {
|
|
29
|
+
configurable: false,
|
|
30
|
+
enumerable: false,
|
|
31
|
+
value: true,
|
|
32
|
+
writable: false,
|
|
33
|
+
});
|
|
34
|
+
return node;
|
|
35
|
+
}
|
|
36
|
+
function cloneSecrets(secrets) {
|
|
37
|
+
return { ...secrets };
|
|
38
|
+
}
|
|
39
|
+
function cloneScope(scope) {
|
|
40
|
+
return scope === undefined ? undefined : [...scope];
|
|
41
|
+
}
|
|
42
|
+
function registerShutdownDispose(dispose) {
|
|
43
|
+
shutdownDisposers.add(dispose);
|
|
44
|
+
}
|
|
45
|
+
function getProxyConnection(group) {
|
|
46
|
+
return group[proxyConnectionSymbol];
|
|
47
|
+
}
|
|
48
|
+
function setProxyConnection(group, connection) {
|
|
49
|
+
group[proxyConnectionSymbol] = connection;
|
|
50
|
+
}
|
|
51
|
+
function createProxyGroup(parent, name) {
|
|
52
|
+
return markProxyNode({
|
|
53
|
+
kind: "group",
|
|
54
|
+
name,
|
|
55
|
+
description: undefined,
|
|
56
|
+
aliases: [],
|
|
57
|
+
scope: cloneScope(parent.scope),
|
|
58
|
+
secrets: cloneSecrets(parent.secrets),
|
|
59
|
+
requires: parent.requires,
|
|
60
|
+
children: [],
|
|
61
|
+
default: undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function createProxyCommand(parent, tool, commandName, connection) {
|
|
65
|
+
const params = convertJsonSchema(tool.inputSchema);
|
|
66
|
+
if (params.kind !== "object") {
|
|
67
|
+
throw new Error(`upstream tool "${tool.name}" must define an object input schema`);
|
|
68
|
+
}
|
|
69
|
+
return markProxyNode({
|
|
70
|
+
kind: "command",
|
|
71
|
+
name: commandName,
|
|
72
|
+
description: tool.description,
|
|
73
|
+
aliases: [],
|
|
74
|
+
positional: [],
|
|
75
|
+
params,
|
|
76
|
+
secrets: cloneSecrets(parent.secrets),
|
|
77
|
+
scope: cloneScope(parent.scope) ?? ["cli", "sdk"],
|
|
78
|
+
confirm: false,
|
|
79
|
+
requires: parent.requires,
|
|
80
|
+
handler: async (ctx) => {
|
|
81
|
+
const client = await ensureConnected(connection);
|
|
82
|
+
return client.callTool({
|
|
83
|
+
name: tool.name,
|
|
84
|
+
arguments: ctx.params,
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
render: undefined,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function removeProxyChildren(group) {
|
|
91
|
+
group.children = group.children.filter((child) => !isProxyNode(child));
|
|
92
|
+
for (const child of group.children) {
|
|
93
|
+
if (child.kind === "group") {
|
|
94
|
+
removeProxyChildren(child);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function findChild(group, name) {
|
|
99
|
+
return group.children.find((child) => child.name === name);
|
|
100
|
+
}
|
|
101
|
+
function filterAllowlistedTools(tools, allowlist) {
|
|
102
|
+
if (allowlist === undefined) {
|
|
103
|
+
return tools;
|
|
104
|
+
}
|
|
105
|
+
const allowedNames = new Set(allowlist);
|
|
106
|
+
return tools.filter((tool) => allowedNames.has(tool.name));
|
|
107
|
+
}
|
|
108
|
+
function validateRenameMap(name, tools, rename) {
|
|
109
|
+
if (rename === undefined) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const toolNames = new Set(tools.map((tool) => tool.name));
|
|
113
|
+
for (const upstreamToolName of Object.keys(rename)) {
|
|
114
|
+
if (!toolNames.has(upstreamToolName)) {
|
|
115
|
+
throw new Error(`couldn't discover MCP ${name}: rename references unknown upstream tool "${upstreamToolName}"`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function createConnection(name, config) {
|
|
120
|
+
const connection = {
|
|
121
|
+
name,
|
|
122
|
+
config,
|
|
123
|
+
async dispose() {
|
|
124
|
+
shutdownDisposers.delete(connection.dispose);
|
|
125
|
+
connection.connecting = undefined;
|
|
126
|
+
if (connection.client === undefined) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const client = connection.client;
|
|
130
|
+
connection.client = undefined;
|
|
131
|
+
await client.close();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
registerShutdownDispose(connection.dispose);
|
|
135
|
+
return connection;
|
|
136
|
+
}
|
|
137
|
+
async function ensureConnected(connection) {
|
|
138
|
+
if (connection.client !== undefined && connection.client.state === "ready") {
|
|
139
|
+
return connection.client;
|
|
140
|
+
}
|
|
141
|
+
if (connection.connecting !== undefined) {
|
|
142
|
+
return connection.connecting;
|
|
143
|
+
}
|
|
144
|
+
connection.connecting = dialUpstream(connection.name, connection.config)
|
|
145
|
+
.then((client) => {
|
|
146
|
+
connection.client = client;
|
|
147
|
+
return client;
|
|
148
|
+
})
|
|
149
|
+
.finally(() => {
|
|
150
|
+
connection.connecting = undefined;
|
|
151
|
+
});
|
|
152
|
+
return connection.connecting;
|
|
153
|
+
}
|
|
154
|
+
async function readCache(cachePath) {
|
|
155
|
+
try {
|
|
156
|
+
const raw = await readFile(cachePath, "utf8");
|
|
157
|
+
const parsed = JSON.parse(raw);
|
|
158
|
+
if (parsed === null ||
|
|
159
|
+
typeof parsed !== "object" ||
|
|
160
|
+
!Array.isArray(parsed.tools) ||
|
|
161
|
+
parsed.upstream === undefined ||
|
|
162
|
+
typeof parsed.upstream.name !== "string" ||
|
|
163
|
+
typeof parsed.upstream.version !== "string") {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
$schema: typeof parsed.$schema === "string" ? parsed.$schema : MCP_PROXY_SCHEMA_URL,
|
|
168
|
+
fetchedAt: typeof parsed.fetchedAt === "string" ? parsed.fetchedAt : new Date(0).toISOString(),
|
|
169
|
+
tools: parsed.tools,
|
|
170
|
+
upstream: parsed.upstream,
|
|
171
|
+
version: parsed.version === 1 ? 1 : 1,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
const code = error.code;
|
|
176
|
+
if (code === "ENOENT" || error instanceof SyntaxError) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function writeCache(cachePath, cache) {
|
|
183
|
+
const directory = path.dirname(cachePath);
|
|
184
|
+
const tempPath = `${cachePath}.tmp`;
|
|
185
|
+
await mkdir(directory, { recursive: true });
|
|
186
|
+
await writeFile(tempPath, `${JSON.stringify(cache, null, 2)}\n`);
|
|
187
|
+
await rename(tempPath, cachePath);
|
|
188
|
+
}
|
|
189
|
+
async function fetchCache(name, config, cachePath) {
|
|
190
|
+
const logger = createLogger((message) => {
|
|
191
|
+
process.stderr.write(`${message}\n`);
|
|
192
|
+
});
|
|
193
|
+
logger.info(`MCP ${name}: connecting`);
|
|
194
|
+
const client = await dialUpstream(name, config);
|
|
195
|
+
try {
|
|
196
|
+
logger.info(`MCP ${name}: listing tools`);
|
|
197
|
+
const tools = [];
|
|
198
|
+
let cursor;
|
|
199
|
+
do {
|
|
200
|
+
const page = await client.listTools(cursor === undefined ? {} : { cursor });
|
|
201
|
+
tools.push(...page.tools);
|
|
202
|
+
cursor = page.nextCursor;
|
|
203
|
+
} while (cursor !== undefined);
|
|
204
|
+
logger.info(`MCP ${name}: found ${tools.length} tools`);
|
|
205
|
+
const upstream = client.serverInfo ?? {
|
|
206
|
+
name,
|
|
207
|
+
version: "unknown",
|
|
208
|
+
};
|
|
209
|
+
const cache = {
|
|
210
|
+
$schema: MCP_PROXY_SCHEMA_URL,
|
|
211
|
+
version: 1,
|
|
212
|
+
upstream,
|
|
213
|
+
fetchedAt: new Date().toISOString(),
|
|
214
|
+
tools,
|
|
215
|
+
};
|
|
216
|
+
await writeCache(cachePath, cache);
|
|
217
|
+
logger.info(`MCP ${name}: wrote ${cachePath}`);
|
|
218
|
+
return cache;
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
await client.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function deleteCacheIfPresent(cachePath) {
|
|
225
|
+
try {
|
|
226
|
+
await unlink(cachePath);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (error.code !== "ENOENT") {
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function populateGroupFromTools(group, tools, rename, connection) {
|
|
235
|
+
removeProxyChildren(group);
|
|
236
|
+
for (const tool of tools) {
|
|
237
|
+
const targetPath = rename?.[tool.name] ?? tool.name;
|
|
238
|
+
const segments = rename !== undefined && Object.prototype.hasOwnProperty.call(rename, tool.name)
|
|
239
|
+
? targetPath.split(".")
|
|
240
|
+
: [tool.name];
|
|
241
|
+
const commandName = segments[segments.length - 1];
|
|
242
|
+
if (commandName === undefined || commandName.length === 0) {
|
|
243
|
+
throw new Error(`command path "${targetPath}" collides with an existing child`);
|
|
244
|
+
}
|
|
245
|
+
let parent = group;
|
|
246
|
+
for (const segment of segments.slice(0, -1)) {
|
|
247
|
+
const existing = findChild(parent, segment);
|
|
248
|
+
if (existing === undefined) {
|
|
249
|
+
const created = createProxyGroup(parent, segment);
|
|
250
|
+
parent.children.push(created);
|
|
251
|
+
parent = created;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (existing.kind !== "group") {
|
|
255
|
+
throw new Error(`command path "${targetPath}" collides with an existing child`);
|
|
256
|
+
}
|
|
257
|
+
parent = existing;
|
|
258
|
+
}
|
|
259
|
+
if (findChild(parent, commandName) !== undefined) {
|
|
260
|
+
throw new Error(`command path "${targetPath}" collides with an existing child`);
|
|
261
|
+
}
|
|
262
|
+
parent.children.push(createProxyCommand(parent, tool, commandName, connection));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function isRefreshRequested(name, refresh) {
|
|
266
|
+
if (refresh === "all") {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
return refresh?.has(name) === true;
|
|
270
|
+
}
|
|
271
|
+
async function resolveSingleProxy(group) {
|
|
272
|
+
const internal = getInternalGroupConfig(group);
|
|
273
|
+
const config = internal.mcp;
|
|
274
|
+
if (config === undefined) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const name = group.name;
|
|
278
|
+
try {
|
|
279
|
+
const cachePath = resolveCachePath(name);
|
|
280
|
+
const refresh = parseRefreshEnv(process.env.TOOLCRAFT_MCP_REFRESH);
|
|
281
|
+
let cache;
|
|
282
|
+
if (isRefreshRequested(name, refresh)) {
|
|
283
|
+
await deleteCacheIfPresent(cachePath);
|
|
284
|
+
cache = await fetchCache(name, config, cachePath);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
cache = (await readCache(cachePath)) ?? (await fetchCache(name, config, cachePath));
|
|
288
|
+
}
|
|
289
|
+
const tools = filterAllowlistedTools(cache.tools, internal.tools);
|
|
290
|
+
validateRenameMap(name, tools, internal.rename);
|
|
291
|
+
const previousConnection = getProxyConnection(group);
|
|
292
|
+
const nextConnection = createConnection(name, config);
|
|
293
|
+
try {
|
|
294
|
+
populateGroupFromTools(group, tools, internal.rename, nextConnection);
|
|
295
|
+
setProxyConnection(group, nextConnection);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
await nextConnection.dispose();
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
if (previousConnection !== undefined && previousConnection !== nextConnection) {
|
|
302
|
+
await previousConnection.dispose();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
if (error instanceof Error && error.message.startsWith(`couldn't discover MCP ${name}:`)) {
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
throw new Error(`couldn't discover MCP ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function collectProxyGroups(root) {
|
|
313
|
+
const groups = [];
|
|
314
|
+
function visit(group) {
|
|
315
|
+
if (getInternalGroupConfig(group).mcp !== undefined) {
|
|
316
|
+
groups.push(group);
|
|
317
|
+
}
|
|
318
|
+
for (const child of group.children) {
|
|
319
|
+
if (child.kind === "group") {
|
|
320
|
+
visit(child);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
visit(root);
|
|
325
|
+
return groups;
|
|
326
|
+
}
|
|
327
|
+
export function hasMcpProxyGroups(root) {
|
|
328
|
+
return collectProxyGroups(root).length > 0;
|
|
329
|
+
}
|
|
330
|
+
export function resolveCachePath(name, projectRoot) {
|
|
331
|
+
if (projectRoot !== undefined) {
|
|
332
|
+
return path.join(projectRoot, ".toolcraft", "mcp", `${name}.json`);
|
|
333
|
+
}
|
|
334
|
+
let current = process.cwd();
|
|
335
|
+
while (true) {
|
|
336
|
+
if (existsSync(path.join(current, "package.json"))) {
|
|
337
|
+
return path.join(current, ".toolcraft", "mcp", `${name}.json`);
|
|
338
|
+
}
|
|
339
|
+
const parent = path.dirname(current);
|
|
340
|
+
if (parent === current) {
|
|
341
|
+
throw new Error(`Could not find package.json above "${process.cwd()}" while resolving MCP cache path.`);
|
|
342
|
+
}
|
|
343
|
+
current = parent;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
export function parseRefreshEnv(value) {
|
|
347
|
+
const trimmed = value?.trim();
|
|
348
|
+
if (trimmed === undefined || trimmed.length === 0) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
if (trimmed === "1" || trimmed === "true") {
|
|
352
|
+
return "all";
|
|
353
|
+
}
|
|
354
|
+
const names = trimmed
|
|
355
|
+
.split(",")
|
|
356
|
+
.map((entry) => entry.trim())
|
|
357
|
+
.filter((entry) => entry.length > 0);
|
|
358
|
+
return names.length === 0 ? undefined : new Set(names);
|
|
359
|
+
}
|
|
360
|
+
export async function dialUpstream(name, config) {
|
|
361
|
+
const client = new McpClient({
|
|
362
|
+
clientInfo: {
|
|
363
|
+
name: `${DEFAULT_CLIENT_INFO.name}-${name}`,
|
|
364
|
+
version: DEFAULT_CLIENT_INFO.version,
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
const transport = config.transport === "stdio"
|
|
368
|
+
? new StdioTransport({
|
|
369
|
+
command: config.command,
|
|
370
|
+
...(config.args === undefined ? {} : { args: config.args }),
|
|
371
|
+
...(config.env === undefined ? {} : { env: config.env }),
|
|
372
|
+
})
|
|
373
|
+
: new HttpTransport({
|
|
374
|
+
url: config.url,
|
|
375
|
+
...(config.headers === undefined ? {} : { headers: config.headers }),
|
|
376
|
+
});
|
|
377
|
+
await client.connect(transport);
|
|
378
|
+
return client;
|
|
379
|
+
}
|
|
380
|
+
export async function resolveMcpProxies(root) {
|
|
381
|
+
const groups = collectProxyGroups(root);
|
|
382
|
+
await Promise.all(groups.map((group) => resolveSingleProxy(group)));
|
|
383
|
+
}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SDKTransport, type Server as TinyServer } from "tiny-stdio-mcp-server";
|
|
2
2
|
import type { Group } from "./index.js";
|
|
3
|
+
import { type HumanInLoopRuntimeOptions } from "./human-in-loop/index.js";
|
|
3
4
|
type Casing = "snake" | "camel";
|
|
4
5
|
type CmdkitServer = Omit<TinyServer, "connect"> & {
|
|
5
6
|
connect(transport: SDKTransport): Promise<void>;
|
|
@@ -7,6 +8,7 @@ type CmdkitServer = Omit<TinyServer, "connect"> & {
|
|
|
7
8
|
export interface RunMCPOptions<TServices extends object = Record<string, unknown>> {
|
|
8
9
|
name: string;
|
|
9
10
|
version: string;
|
|
11
|
+
humanInLoop?: HumanInLoopRuntimeOptions;
|
|
10
12
|
/**
|
|
11
13
|
* Optional allowlist of MCP tool names or group prefixes.
|
|
12
14
|
*
|
package/dist/mcp.js
CHANGED
|
@@ -2,6 +2,9 @@ import { access, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { createServer, JSON_RPC_ERROR_CODES, ToolError, } from "tiny-stdio-mcp-server";
|
|
3
3
|
import { toJsonSchema } from "toolcraft-schema";
|
|
4
4
|
import { UserError, assertCommandRequirements, resolveCommandSecrets } from "./index.js";
|
|
5
|
+
import { mergeApprovalsGroup } from "./human-in-loop/approvals-commands.js";
|
|
6
|
+
import { ApprovalDeclinedError, invokeWithHumanInLoop, } from "./human-in-loop/index.js";
|
|
7
|
+
import { hasMcpProxyGroups, resolveMcpProxies } from "./mcp-proxy.js";
|
|
5
8
|
import { getExpectedNumberDescription, isValidNumberSchemaValue } from "./number-schema.js";
|
|
6
9
|
import { filterSchemaForScope } from "./schema-scope.js";
|
|
7
10
|
const RESERVED_SERVICE_NAMES = new Set(["params", "secrets", "fetch", "fs", "env", "progress"]);
|
|
@@ -160,12 +163,12 @@ function formatToolName(path) {
|
|
|
160
163
|
}
|
|
161
164
|
function enumerateTools(root, casing, allowlist) {
|
|
162
165
|
const tools = [];
|
|
163
|
-
function visit(node,
|
|
166
|
+
function visit(node, toolPath, commandPath) {
|
|
164
167
|
if (node.kind === "command") {
|
|
165
168
|
if (!node.scope.includes("mcp")) {
|
|
166
169
|
return;
|
|
167
170
|
}
|
|
168
|
-
const name = formatToolName([...
|
|
171
|
+
const name = formatToolName([...toolPath, node.name]);
|
|
169
172
|
const params = filterSchemaForScope(node.params, "mcp");
|
|
170
173
|
if (!matchesAllowlist(name, allowlist)) {
|
|
171
174
|
return;
|
|
@@ -175,23 +178,67 @@ function enumerateTools(root, casing, allowlist) {
|
|
|
175
178
|
}
|
|
176
179
|
tools.push({
|
|
177
180
|
command: node,
|
|
181
|
+
commandPath: [...commandPath, node.name].join("."),
|
|
178
182
|
name,
|
|
179
183
|
description: buildToolDescription(node.description, params, casing),
|
|
180
184
|
inputSchema: applySchemaCasing(toJsonSchema(params), casing),
|
|
181
185
|
});
|
|
182
186
|
return;
|
|
183
187
|
}
|
|
184
|
-
const
|
|
188
|
+
const nextToolPath = [...toolPath, node.name];
|
|
189
|
+
const nextCommandPath = [...commandPath, node.name];
|
|
185
190
|
for (const child of node.children) {
|
|
186
|
-
visit(child,
|
|
191
|
+
visit(child, nextToolPath, nextCommandPath);
|
|
187
192
|
}
|
|
188
193
|
}
|
|
189
194
|
const rootPath = root.name.length === 0 ? [] : [root.name];
|
|
190
195
|
for (const child of root.children) {
|
|
191
|
-
visit(child, rootPath);
|
|
196
|
+
visit(child, rootPath, []);
|
|
192
197
|
}
|
|
193
198
|
return tools;
|
|
194
199
|
}
|
|
200
|
+
function isHumanInLoopPending(result) {
|
|
201
|
+
return (typeof result === "object" &&
|
|
202
|
+
result !== null &&
|
|
203
|
+
result.status === "pending-approval" &&
|
|
204
|
+
typeof result.approvalId === "string" &&
|
|
205
|
+
typeof result.message === "string" &&
|
|
206
|
+
typeof result.enqueuedAt === "string");
|
|
207
|
+
}
|
|
208
|
+
function renderPendingApproval(pending) {
|
|
209
|
+
return {
|
|
210
|
+
isError: false,
|
|
211
|
+
content: [
|
|
212
|
+
{
|
|
213
|
+
type: "text",
|
|
214
|
+
text: `Queued for human approval (id: ${pending.approvalId}). Track with \`toolcraft approvals show ${pending.approvalId}\`.`,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: JSON.stringify(pending),
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function renderDeclinedApproval(error) {
|
|
224
|
+
return {
|
|
225
|
+
isError: true,
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: "text",
|
|
229
|
+
text: error.reason === undefined ? "Declined." : `Declined: ${error.reason}`,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
type: "text",
|
|
233
|
+
text: JSON.stringify({
|
|
234
|
+
outcome: "declined",
|
|
235
|
+
reason: error.reason,
|
|
236
|
+
commandPath: error.commandPath,
|
|
237
|
+
}),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
195
242
|
function validateEnum(value, schema, label) {
|
|
196
243
|
if (!schema.values.includes(value)) {
|
|
197
244
|
throw new UserError(`Invalid value for "${label}". Expected one of: ${schema.values.map((candidate) => String(candidate)).join(", ")}.`);
|
|
@@ -307,10 +354,15 @@ function toToolError(error) {
|
|
|
307
354
|
}
|
|
308
355
|
return new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, String(error));
|
|
309
356
|
}
|
|
310
|
-
|
|
311
|
-
const root = normalizeRoots(roots);
|
|
357
|
+
function createResolvedMCPServer(root, options) {
|
|
312
358
|
const casing = options.casing ?? "snake";
|
|
313
359
|
const services = (options.services ?? {});
|
|
360
|
+
const runtimeOptions = options.humanInLoop ?? {};
|
|
361
|
+
const servicesWithBuiltIns = {
|
|
362
|
+
...services,
|
|
363
|
+
runtimeOptions,
|
|
364
|
+
root,
|
|
365
|
+
};
|
|
314
366
|
validateServices(services);
|
|
315
367
|
const tools = enumerateTools(root, casing, options.tools);
|
|
316
368
|
const server = createServer({ name: options.name, version: options.version });
|
|
@@ -319,7 +371,7 @@ export function createMCPServer(roots, options) {
|
|
|
319
371
|
try {
|
|
320
372
|
const secrets = resolveCommandSecrets(tool.command);
|
|
321
373
|
const baseContext = {
|
|
322
|
-
...
|
|
374
|
+
...servicesWithBuiltIns,
|
|
323
375
|
secrets,
|
|
324
376
|
fetch: globalThis.fetch,
|
|
325
377
|
fs: createFs(),
|
|
@@ -330,13 +382,19 @@ export function createMCPServer(roots, options) {
|
|
|
330
382
|
};
|
|
331
383
|
await assertCommandRequirements(tool.command, { ...baseContext, params: undefined });
|
|
332
384
|
const params = validateToolArguments(tool.command.params, argumentsValue, casing);
|
|
333
|
-
const result = await tool.command
|
|
385
|
+
const result = await invokeWithHumanInLoop(tool.command, {
|
|
334
386
|
...baseContext,
|
|
335
387
|
params,
|
|
336
|
-
});
|
|
388
|
+
}, runtimeOptions, tool.commandPath);
|
|
389
|
+
if (isHumanInLoopPending(result)) {
|
|
390
|
+
return renderPendingApproval(result);
|
|
391
|
+
}
|
|
337
392
|
return toToolContent(result);
|
|
338
393
|
}
|
|
339
394
|
catch (error) {
|
|
395
|
+
if (error instanceof ApprovalDeclinedError) {
|
|
396
|
+
return renderDeclinedApproval(error);
|
|
397
|
+
}
|
|
340
398
|
throw toToolError(error);
|
|
341
399
|
}
|
|
342
400
|
});
|
|
@@ -348,7 +406,41 @@ export function createMCPServer(roots, options) {
|
|
|
348
406
|
},
|
|
349
407
|
};
|
|
350
408
|
}
|
|
409
|
+
function createDeferredMCPServer(root, options) {
|
|
410
|
+
let serverPromise;
|
|
411
|
+
const resolveServer = () => {
|
|
412
|
+
serverPromise ??= (async () => {
|
|
413
|
+
await resolveMcpProxies(root);
|
|
414
|
+
return createResolvedMCPServer(root, options);
|
|
415
|
+
})();
|
|
416
|
+
return serverPromise;
|
|
417
|
+
};
|
|
418
|
+
return new Proxy({
|
|
419
|
+
listen() {
|
|
420
|
+
return resolveServer().then((server) => server.listen());
|
|
421
|
+
},
|
|
422
|
+
connect(transport) {
|
|
423
|
+
return resolveServer().then((server) => server.connect(transport));
|
|
424
|
+
},
|
|
425
|
+
}, {
|
|
426
|
+
get(target, property, receiver) {
|
|
427
|
+
if (property === "then") {
|
|
428
|
+
return resolveServer().then.bind(resolveServer());
|
|
429
|
+
}
|
|
430
|
+
return Reflect.get(target, property, receiver);
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
export function createMCPServer(roots, options) {
|
|
435
|
+
const root = mergeApprovalsGroup(normalizeRoots(roots));
|
|
436
|
+
if (!hasMcpProxyGroups(root)) {
|
|
437
|
+
return createResolvedMCPServer(root, options);
|
|
438
|
+
}
|
|
439
|
+
return createDeferredMCPServer(root, options);
|
|
440
|
+
}
|
|
351
441
|
export async function runMCP(roots, options) {
|
|
352
|
-
const
|
|
442
|
+
const root = mergeApprovalsGroup(normalizeRoots(roots));
|
|
443
|
+
await resolveMcpProxies(root);
|
|
444
|
+
const server = createResolvedMCPServer(root, options);
|
|
353
445
|
await server.listen();
|
|
354
446
|
}
|
|
@@ -39,6 +39,49 @@ const ignoredRoot = defineGroup({
|
|
|
39
39
|
params: S.Object({}),
|
|
40
40
|
handler: async () => "hidden",
|
|
41
41
|
}),
|
|
42
|
+
defineCommand({
|
|
43
|
+
name: "queued",
|
|
44
|
+
scope: ["sdk"],
|
|
45
|
+
params: S.Object({
|
|
46
|
+
prompt_text: S.String(),
|
|
47
|
+
}),
|
|
48
|
+
humanInLoop: {
|
|
49
|
+
mode: "async",
|
|
50
|
+
message: () => "queue it",
|
|
51
|
+
},
|
|
52
|
+
handler: async ({ params }) => ({
|
|
53
|
+
content: params.prompt_text,
|
|
54
|
+
}),
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
}),
|
|
58
|
+
defineGroup({
|
|
59
|
+
name: "review",
|
|
60
|
+
scope: ["sdk"],
|
|
61
|
+
humanInLoop: {
|
|
62
|
+
mode: "async",
|
|
63
|
+
message: () => "needs review",
|
|
64
|
+
},
|
|
65
|
+
children: [
|
|
66
|
+
defineCommand({
|
|
67
|
+
name: "submit",
|
|
68
|
+
params: S.Object({
|
|
69
|
+
target_name: S.String(),
|
|
70
|
+
}),
|
|
71
|
+
handler: async ({ params }) => ({
|
|
72
|
+
target: params.target_name,
|
|
73
|
+
}),
|
|
74
|
+
}),
|
|
75
|
+
defineCommand({
|
|
76
|
+
name: "skip",
|
|
77
|
+
params: S.Object({
|
|
78
|
+
target_name: S.String(),
|
|
79
|
+
}),
|
|
80
|
+
humanInLoop: null,
|
|
81
|
+
handler: async ({ params }) => ({
|
|
82
|
+
target: params.target_name,
|
|
83
|
+
}),
|
|
84
|
+
}),
|
|
42
85
|
],
|
|
43
86
|
}),
|
|
44
87
|
],
|
|
@@ -50,6 +93,7 @@ const ignoredOptions = {
|
|
|
50
93
|
services: {
|
|
51
94
|
logger: console,
|
|
52
95
|
},
|
|
96
|
+
humanInLoop: {},
|
|
53
97
|
};
|
|
54
98
|
const ignoredSdk = createSDK(ignoredRoot, ignoredOptions);
|
|
55
99
|
const ignoredResult = ignoredSdk.poeCode.generate.text({
|
|
@@ -67,6 +111,27 @@ const ignoredHttpServerResult = ignoredSdk.poeCode.generate.httpServer({
|
|
|
67
111
|
void ignoredHttpServerResult.then((value) => {
|
|
68
112
|
void value.apiKey;
|
|
69
113
|
});
|
|
114
|
+
const ignoredQueuedResult = ignoredSdk.poeCode.generate.queued({
|
|
115
|
+
promptText: "hello",
|
|
116
|
+
});
|
|
117
|
+
void ignoredQueuedResult.then((value) => {
|
|
118
|
+
const pending = value;
|
|
119
|
+
void pending.approvalId;
|
|
120
|
+
void pending.message;
|
|
121
|
+
});
|
|
122
|
+
const ignoredInheritedAsyncResult = ignoredSdk.poeCode.review.submit({
|
|
123
|
+
targetName: "prod",
|
|
124
|
+
});
|
|
125
|
+
void ignoredInheritedAsyncResult.then((value) => {
|
|
126
|
+
void value.status;
|
|
127
|
+
void value.enqueuedAt;
|
|
128
|
+
});
|
|
129
|
+
const ignoredOptedOutResult = ignoredSdk.poeCode.review.skip({
|
|
130
|
+
targetName: "prod",
|
|
131
|
+
});
|
|
132
|
+
void ignoredOptedOutResult.then((value) => {
|
|
133
|
+
void value.target;
|
|
134
|
+
});
|
|
70
135
|
// @ts-expect-error cli-only commands are not exposed in the SDK surface
|
|
71
136
|
void ignoredSdk.poeCode.generate.cliOnly;
|
|
72
137
|
// @ts-expect-error wrong parameter name
|
|
@@ -77,3 +142,15 @@ ignoredSdk.poeCode.generate.text({ promptText: 123 });
|
|
|
77
142
|
void ignoredSdk.poeCode.generate.hTTPServer;
|
|
78
143
|
// @ts-expect-error acronym parameter names should camel-case cleanly
|
|
79
144
|
ignoredSdk.poeCode.generate.httpServer({ aPIKey: "secret" });
|
|
145
|
+
void ignoredQueuedResult.then((value) => {
|
|
146
|
+
// @ts-expect-error async human-in-loop commands return the pending marker, not the handler result
|
|
147
|
+
void value.content;
|
|
148
|
+
});
|
|
149
|
+
void ignoredInheritedAsyncResult.then((value) => {
|
|
150
|
+
// @ts-expect-error inherited async human-in-loop mode also returns the pending marker
|
|
151
|
+
void value.target;
|
|
152
|
+
});
|
|
153
|
+
void ignoredOptedOutResult.then((value) => {
|
|
154
|
+
// @ts-expect-error opting out of inherited human-in-loop keeps the handler result type
|
|
155
|
+
void value.approvalId;
|
|
156
|
+
});
|