stitchkit 0.53.2 → 0.55.0
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 +4 -1
- package/dist/browser/socket-io.d.ts +11 -1
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +15 -8
- package/dist/contract/define.d.ts +27 -0
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/file-ref.d.ts +13 -0
- package/dist/contract/file-ref.d.ts.map +1 -0
- package/dist/contract/index.d.ts +2 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +21 -17
- package/dist/files/boundary.d.ts +52 -0
- package/dist/files/boundary.d.ts.map +1 -0
- package/dist/files.d.ts +3 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +16 -0
- package/dist/{index-c78w2y09.js → index-0gv7k0ra.js} +515 -156
- package/dist/index-1bx83sw4.js +4 -0
- package/dist/index-1k16zv57.js +269 -0
- package/dist/index-6k1937bx.js +20 -0
- package/dist/{index-bmnhqtya.js → index-7eq9va6e.js} +1 -4
- package/dist/{index-yxpe3phd.js → index-cby4ar3v.js} +1 -18
- package/dist/index-h60df7rj.js +110 -0
- package/dist/{index-yez419px.js → index-js7yexng.js} +19 -6
- package/dist/index-k4dftwf7.js +66 -0
- package/dist/{index-pdv1mjjr.js → index-npye5kt7.js} +30 -59
- package/dist/{index-yydqk4fh.js → index-pzc32v75.js} +90 -3
- package/dist/{index-e1es808r.js → index-qh4xevf5.js} +1 -1
- package/dist/{index-gvha32jh.js → index-qx84bqzk.js} +2 -2
- package/dist/{index-pfqjb5xy.js → index-rgd18hx3.js} +23 -5
- package/dist/index-sbdmyz75.js +18 -0
- package/dist/{index-173tvqcp.js → index-v4bbq3p4.js} +9 -114
- package/dist/index.js +162 -35
- package/dist/node.js +30 -27
- package/dist/observability/event.d.ts +2 -16
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.js +28 -25
- package/dist/react.js +2 -2
- package/dist/realtime/contract.d.ts +9 -0
- package/dist/realtime/contract.d.ts.map +1 -1
- package/dist/realtime/index.d.ts +2 -1
- package/dist/realtime/index.d.ts.map +1 -1
- package/dist/realtime/request.d.ts +21 -0
- package/dist/realtime/request.d.ts.map +1 -0
- package/dist/realtime/socket.d.ts +3 -0
- package/dist/realtime/socket.d.ts.map +1 -1
- package/dist/remote.d.ts +9 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +79 -0
- package/dist/server/context-contribution.d.ts +10 -0
- package/dist/server/context-contribution.d.ts.map +1 -0
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +133 -88
- package/dist/server/lifecycle.d.ts +4 -0
- package/dist/server/lifecycle.d.ts.map +1 -0
- package/dist/server/middleware/auth.d.ts +35 -7
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/process-signal-common.d.ts +6 -0
- package/dist/server/process-signal-common.d.ts.map +1 -0
- package/dist/server/process-signals.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/testing/surface-conformance.d.ts +47 -0
- package/dist/testing/surface-conformance.d.ts.map +1 -0
- package/dist/testing/surface-manifest.d.ts +156 -0
- package/dist/testing/surface-manifest.d.ts.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +366 -3
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/async-operation.d.ts +168 -0
- package/dist/tools/async-operation.d.ts.map +1 -0
- package/dist/tools/cli-args.d.ts +1 -1
- package/dist/tools/cli-args.d.ts.map +1 -1
- package/dist/tools/cli-command.d.ts +29 -0
- package/dist/tools/cli-command.d.ts.map +1 -0
- package/dist/tools/cli-format.d.ts +2 -2
- package/dist/tools/cli-format.d.ts.map +1 -1
- package/dist/tools/cli-wait.d.ts +11 -2
- package/dist/tools/cli-wait.d.ts.map +1 -1
- package/dist/tools/cli.d.ts +13 -6
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/define-download-tool.d.ts +18 -0
- package/dist/tools/define-download-tool.d.ts.map +1 -0
- package/dist/tools/define-upload-tool.d.ts +16 -0
- package/dist/tools/define-upload-tool.d.ts.map +1 -0
- package/dist/tools/define-view-file-tool.d.ts +10 -0
- package/dist/tools/define-view-file-tool.d.ts.map +1 -0
- package/dist/tools/define-wait-tool.d.ts +22 -0
- package/dist/tools/define-wait-tool.d.ts.map +1 -0
- package/dist/tools/download-core.d.ts +20 -0
- package/dist/tools/download-core.d.ts.map +1 -0
- package/dist/tools/execute.d.ts +3 -1
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/lifecycle.d.ts +4 -0
- package/dist/tools/lifecycle.d.ts.map +1 -0
- package/dist/tools/mcp-round.d.ts +0 -1
- package/dist/tools/mcp-round.d.ts.map +1 -1
- package/dist/tools/mcp-stdio-signals.d.ts +33 -0
- package/dist/tools/mcp-stdio-signals.d.ts.map +1 -0
- package/dist/tools/mount-download.d.ts +10 -4
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/mount-upload.d.ts +4 -2
- package/dist/tools/mount-upload.d.ts.map +1 -1
- package/dist/tools/mount-wait.d.ts.map +1 -1
- package/dist/tools/native-definition.d.ts +15 -0
- package/dist/tools/native-definition.d.ts.map +1 -0
- package/dist/tools/runtime-tool.d.ts +5 -4
- package/dist/tools/runtime-tool.d.ts.map +1 -1
- package/dist/tools/surface.d.ts.map +1 -1
- package/dist/tools/upload-core.d.ts +4 -0
- package/dist/tools/upload-core.d.ts.map +1 -0
- package/dist/tools/view-file.d.ts +84 -23
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools/wait-core.d.ts +16 -2
- package/dist/tools/wait-core.d.ts.map +1 -1
- package/dist/tools.d.ts +12 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +827 -495
- package/llms-full.txt +720 -53
- package/package.json +10 -2
package/dist/tools.js
CHANGED
|
@@ -1,201 +1,72 @@
|
|
|
1
1
|
import {
|
|
2
2
|
signJwt,
|
|
3
3
|
verifyPkce
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-js7yexng.js";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_CORS_ALLOW_HEADERS,
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
DEFAULT_PROCESS_SIGNALS,
|
|
8
|
+
contractOnlyService,
|
|
9
|
+
defaultSignalSource,
|
|
10
|
+
guardSignalCallback,
|
|
11
|
+
reportSignalError
|
|
12
|
+
} from "./index-pzc32v75.js";
|
|
9
13
|
import {
|
|
10
|
-
|
|
11
|
-
assertUniqueToolName,
|
|
14
|
+
WaitTimeoutError,
|
|
12
15
|
buildToolPresentationSchema,
|
|
13
16
|
coerceJsonArgs,
|
|
17
|
+
collectToolSurface,
|
|
14
18
|
collectTools,
|
|
15
19
|
createCli,
|
|
20
|
+
createRuntimeToolFactory,
|
|
16
21
|
createToolRunner,
|
|
22
|
+
defineCliCommand,
|
|
23
|
+
defineRuntimeTool,
|
|
17
24
|
executeToolMethod,
|
|
18
25
|
fetchGuarded,
|
|
19
26
|
fetchPinnedDocument,
|
|
20
27
|
flattenToolJsonSchema,
|
|
21
28
|
formatToolError,
|
|
22
29
|
isObjectPresentationSchema,
|
|
23
|
-
pollUntil,
|
|
24
30
|
presentationMetadata,
|
|
25
31
|
readCapped,
|
|
32
|
+
runWaitOperation,
|
|
26
33
|
toolErrorFromResult,
|
|
27
|
-
toolResultFromError
|
|
28
|
-
|
|
29
|
-
} from "./index-c78w2y09.js";
|
|
34
|
+
toolResultFromError
|
|
35
|
+
} from "./index-0gv7k0ra.js";
|
|
30
36
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} from "./index-yxpe3phd.js";
|
|
37
|
+
ManagedFileError
|
|
38
|
+
} from "./index-1k16zv57.js";
|
|
39
|
+
import"./index-sbdmyz75.js";
|
|
35
40
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} from "./index-
|
|
39
|
-
import
|
|
41
|
+
assertToolName,
|
|
42
|
+
assertUniqueToolName
|
|
43
|
+
} from "./index-k4dftwf7.js";
|
|
44
|
+
import {
|
|
45
|
+
toJsonSchema
|
|
46
|
+
} from "./index-cby4ar3v.js";
|
|
40
47
|
import {
|
|
41
48
|
redact
|
|
42
|
-
} from "./index-
|
|
49
|
+
} from "./index-qx84bqzk.js";
|
|
43
50
|
import {
|
|
44
|
-
AppError,
|
|
45
51
|
getRequestContext,
|
|
46
52
|
getTraceId,
|
|
47
|
-
mergeMeta,
|
|
48
53
|
runWithRequestContext
|
|
49
|
-
} from "./index-
|
|
54
|
+
} from "./index-v4bbq3p4.js";
|
|
55
|
+
import {
|
|
56
|
+
AppError
|
|
57
|
+
} from "./index-h60df7rj.js";
|
|
58
|
+
import {
|
|
59
|
+
ManagedFilePathSchema,
|
|
60
|
+
ManagedFileRefSchema
|
|
61
|
+
} from "./index-6k1937bx.js";
|
|
50
62
|
import {
|
|
51
63
|
isRecord,
|
|
52
64
|
resolvePropagationContext
|
|
53
|
-
} from "./index-
|
|
65
|
+
} from "./index-7eq9va6e.js";
|
|
66
|
+
import"./index-1bx83sw4.js";
|
|
54
67
|
|
|
55
68
|
// src/tools/agent.ts
|
|
56
69
|
import { jsonSchema, tool } from "ai";
|
|
57
|
-
|
|
58
|
-
// src/tools/runtime-tool.ts
|
|
59
|
-
function defineRuntimeTool(definition) {
|
|
60
|
-
if (definition.transports?.length === 0) {
|
|
61
|
-
throw new Error(`Runtime tool "${definition.name}" must expose at least one transport`);
|
|
62
|
-
}
|
|
63
|
-
return definition;
|
|
64
|
-
}
|
|
65
|
-
function createRuntimeToolFactory(config) {
|
|
66
|
-
function parseContext(context) {
|
|
67
|
-
const parsed = config.context.parse(context);
|
|
68
|
-
return {
|
|
69
|
-
...context,
|
|
70
|
-
...parsed,
|
|
71
|
-
params: undefined,
|
|
72
|
-
input: context.input
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function define(definition) {
|
|
76
|
-
if (definition.output !== undefined) {
|
|
77
|
-
const { action: action2, method: method2, meta: meta2, handler: handler2, ...tool2 } = definition;
|
|
78
|
-
const identity2 = {
|
|
79
|
-
serviceName: config.serviceName,
|
|
80
|
-
action: action2,
|
|
81
|
-
method: method2,
|
|
82
|
-
...config.scope !== undefined && { scope: config.scope },
|
|
83
|
-
...(meta2 ?? config.meta) !== undefined && { meta: meta2 ?? config.meta }
|
|
84
|
-
};
|
|
85
|
-
return defineRuntimeTool({
|
|
86
|
-
...tool2,
|
|
87
|
-
identity: identity2,
|
|
88
|
-
handler: (context) => handler2(parseContext(context))
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
const { action, method, meta, handler, ...tool } = definition;
|
|
92
|
-
const identity = {
|
|
93
|
-
serviceName: config.serviceName,
|
|
94
|
-
action,
|
|
95
|
-
method,
|
|
96
|
-
...config.scope !== undefined && { scope: config.scope },
|
|
97
|
-
...(meta ?? config.meta) !== undefined && { meta: meta ?? config.meta }
|
|
98
|
-
};
|
|
99
|
-
return defineRuntimeTool({
|
|
100
|
-
...tool,
|
|
101
|
-
identity,
|
|
102
|
-
handler: (context) => handler(parseContext(context))
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return { define };
|
|
106
|
-
}
|
|
107
|
-
function runtimeToolSupports(definition, transport) {
|
|
108
|
-
if (definition.transports?.length === 0) {
|
|
109
|
-
throw new Error(`Runtime tool "${definition.name}" must expose at least one transport`);
|
|
110
|
-
}
|
|
111
|
-
return !definition.transports || definition.transports.includes(transport);
|
|
112
|
-
}
|
|
113
|
-
function runtimeToolIdentity(definition) {
|
|
114
|
-
return {
|
|
115
|
-
method: definition.identity.method,
|
|
116
|
-
desc: definition.description,
|
|
117
|
-
serviceName: definition.identity.serviceName,
|
|
118
|
-
key: definition.identity.action,
|
|
119
|
-
toolName: definition.name,
|
|
120
|
-
scope: definition.identity.scope,
|
|
121
|
-
meta: definition.identity.meta,
|
|
122
|
-
annotations: definition.annotations,
|
|
123
|
-
ui: definition.ui,
|
|
124
|
-
mcp: definition.mcp
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function runtimeToolMountable(definition, assertName = true) {
|
|
128
|
-
if (assertName) {
|
|
129
|
-
assertToolName(definition.name, definition.identity.serviceName, definition.identity.action);
|
|
130
|
-
}
|
|
131
|
-
const method = {
|
|
132
|
-
...runtimeToolIdentity(definition),
|
|
133
|
-
inputSchema: definition.input,
|
|
134
|
-
outputSchema: definition.output,
|
|
135
|
-
handler: definition.handler
|
|
136
|
-
};
|
|
137
|
-
return {
|
|
138
|
-
method,
|
|
139
|
-
name: definition.name,
|
|
140
|
-
argumentSchema: definition.input,
|
|
141
|
-
presentationSchema: buildToolPresentationSchema({
|
|
142
|
-
inputSchema: definition.input,
|
|
143
|
-
unrepresentable: "any"
|
|
144
|
-
}),
|
|
145
|
-
shouldExtend: false
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// src/tools/surface.ts
|
|
150
|
-
function duplicateLabel(transport) {
|
|
151
|
-
if (transport === "MCP")
|
|
152
|
-
return "MCP tool name";
|
|
153
|
-
if (transport === "AGENT")
|
|
154
|
-
return "agent tool name";
|
|
155
|
-
return "CLI command";
|
|
156
|
-
}
|
|
157
|
-
function collectToolSurface({
|
|
158
|
-
surface,
|
|
159
|
-
transport,
|
|
160
|
-
assertUniqueNames = true,
|
|
161
|
-
...collectConfig
|
|
162
|
-
}) {
|
|
163
|
-
const entries = [];
|
|
164
|
-
const names = new Set;
|
|
165
|
-
const append = (entry) => {
|
|
166
|
-
if (assertUniqueNames) {
|
|
167
|
-
assertUniqueToolName(entry.mountable.name, names.has(entry.mountable.name), duplicateLabel(transport));
|
|
168
|
-
}
|
|
169
|
-
names.add(entry.mountable.name);
|
|
170
|
-
entries.push(entry);
|
|
171
|
-
};
|
|
172
|
-
for (const service of surface.services ?? []) {
|
|
173
|
-
for (const mountable of collectTools(service, transport, collectConfig)) {
|
|
174
|
-
append({
|
|
175
|
-
kind: "contract",
|
|
176
|
-
service: service.name,
|
|
177
|
-
action: mountable.method.key,
|
|
178
|
-
mountable
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (transport === "CLI")
|
|
183
|
-
return entries;
|
|
184
|
-
for (const definition of surface.runtimeTools ?? []) {
|
|
185
|
-
if (!runtimeToolSupports(definition, transport))
|
|
186
|
-
continue;
|
|
187
|
-
append({
|
|
188
|
-
kind: "runtime",
|
|
189
|
-
service: definition.identity.serviceName,
|
|
190
|
-
action: definition.identity.action,
|
|
191
|
-
mountable: runtimeToolMountable(definition, collectConfig.assertNames),
|
|
192
|
-
definition
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
return entries;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// src/tools/agent.ts
|
|
199
70
|
function mountAgent(services, config = {}) {
|
|
200
71
|
const serviceList = Array.isArray(services) ? services : [services];
|
|
201
72
|
const tools = {};
|
|
@@ -219,10 +90,10 @@ function mountAgent(services, config = {}) {
|
|
|
219
90
|
const inputSchema = jsonSchema(mountable.presentationSchema, {
|
|
220
91
|
validate: async (value) => isRecord(value) ? { success: true, value } : { success: false, error: new Error("Tool arguments must be an object") }
|
|
221
92
|
});
|
|
222
|
-
const execute = async (rawArgs) => {
|
|
93
|
+
const execute = async (rawArgs, options) => {
|
|
223
94
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
224
95
|
try {
|
|
225
|
-
const result = await runTool(mountable, args);
|
|
96
|
+
const result = await runTool(mountable, args, { signal: options.abortSignal });
|
|
226
97
|
if (result.ok)
|
|
227
98
|
return result.data;
|
|
228
99
|
return formatToolError(result, mountable.name, config.errorHint);
|
|
@@ -262,6 +133,600 @@ function mountAgent(services, config = {}) {
|
|
|
262
133
|
}
|
|
263
134
|
return tools;
|
|
264
135
|
}
|
|
136
|
+
// src/tools/async-operation.ts
|
|
137
|
+
import { z } from "zod";
|
|
138
|
+
|
|
139
|
+
// src/tools/native-definition.ts
|
|
140
|
+
function managedNativeIdentity(identity, method) {
|
|
141
|
+
return { ...identity, method };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/tools/define-wait-tool.ts
|
|
145
|
+
function defineWaitTool(config) {
|
|
146
|
+
const renderedMcp = config.render ? {
|
|
147
|
+
mcp: (state) => {
|
|
148
|
+
const rendered = config.render?.(state, false);
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: rendered?.text ?? JSON.stringify(state, null, 2)
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
} : undefined;
|
|
159
|
+
const present = renderedMcp || config.present ? { ...renderedMcp, ...config.present } : undefined;
|
|
160
|
+
return defineRuntimeTool({
|
|
161
|
+
name: config.name ?? "wait",
|
|
162
|
+
description: config.description,
|
|
163
|
+
identity: managedNativeIdentity(config.identity, "GET"),
|
|
164
|
+
input: config.input,
|
|
165
|
+
output: config.state,
|
|
166
|
+
transports: config.transports,
|
|
167
|
+
annotations: config.annotations,
|
|
168
|
+
present,
|
|
169
|
+
handler: async (context) => {
|
|
170
|
+
let result;
|
|
171
|
+
try {
|
|
172
|
+
result = await runWaitOperation({
|
|
173
|
+
input: context.input,
|
|
174
|
+
poll: async (input, signal) => config.state.parse(await config.poll(input, { ...context, signal })),
|
|
175
|
+
done: config.done,
|
|
176
|
+
backoff: config.backoff,
|
|
177
|
+
timeoutSec: config.timeoutFromInput?.(context.input) ?? config.defaultTimeout,
|
|
178
|
+
signal: context.signal
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error instanceof WaitTimeoutError) {
|
|
182
|
+
throw new AppError("WAIT_TIMEOUT", "Wait timed out before the first snapshot", 408);
|
|
183
|
+
}
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
const rendered = config.render?.(result.state, result.timedOut);
|
|
187
|
+
if (result.timedOut) {
|
|
188
|
+
throw new AppError("WAIT_TIMEOUT", rendered?.text ?? "Wait timed out before the operation completed", 408);
|
|
189
|
+
}
|
|
190
|
+
if (rendered?.isError) {
|
|
191
|
+
throw new AppError("WAIT_FAILED", rendered.text, 409);
|
|
192
|
+
}
|
|
193
|
+
return result.state;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/tools/async-operation.ts
|
|
199
|
+
var AsyncOperationCancelResultSchema = z.discriminatedUnion("outcome", [
|
|
200
|
+
z.object({ outcome: z.literal("accepted") }),
|
|
201
|
+
z.object({ outcome: z.literal("already_terminal") }),
|
|
202
|
+
z.object({ outcome: z.literal("rejected"), reason: z.string().min(1) })
|
|
203
|
+
]);
|
|
204
|
+
function createAsyncOperationSnapshotSchema(config) {
|
|
205
|
+
const progress = config.progress ? { progress: config.progress.optional() } : {};
|
|
206
|
+
return z.discriminatedUnion("phase", [
|
|
207
|
+
z.object({ phase: z.literal("pending"), ...progress }),
|
|
208
|
+
z.object({ phase: z.literal("running"), ...progress }),
|
|
209
|
+
z.object({ phase: z.literal("succeeded"), ...progress }),
|
|
210
|
+
z.object({ phase: z.literal("failed"), failure: config.failure, ...progress }),
|
|
211
|
+
z.object({ phase: z.literal("cancelled"), ...progress })
|
|
212
|
+
]);
|
|
213
|
+
}
|
|
214
|
+
function terminal(snapshot) {
|
|
215
|
+
if (typeof snapshot !== "object" || snapshot === null || !("phase" in snapshot)) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return snapshot.phase === "succeeded" || snapshot.phase === "failed" || snapshot.phase === "cancelled";
|
|
219
|
+
}
|
|
220
|
+
function succeeded(snapshot) {
|
|
221
|
+
return typeof snapshot === "object" && snapshot !== null && "phase" in snapshot && snapshot.phase === "succeeded";
|
|
222
|
+
}
|
|
223
|
+
function defineAsyncOperation(config) {
|
|
224
|
+
const capabilityName = (capability) => config.names?.[capability] ?? `${config.name}_${capability}`;
|
|
225
|
+
const description = (capability) => config.descriptions?.[capability] ?? `${config.description}: ${capability}`;
|
|
226
|
+
const identity = (capability, method) => ({
|
|
227
|
+
serviceName: config.identity.serviceName,
|
|
228
|
+
action: `${config.identity.action}.${capability}`,
|
|
229
|
+
scope: config.scopes?.[capability] ?? config.identity.scope,
|
|
230
|
+
meta: config.identity.meta,
|
|
231
|
+
method
|
|
232
|
+
});
|
|
233
|
+
const common = (capability) => ({
|
|
234
|
+
name: capabilityName(capability),
|
|
235
|
+
description: description(capability),
|
|
236
|
+
transports: config.transports?.[capability],
|
|
237
|
+
annotations: config.annotations?.[capability] ?? (capability === "start" || capability === "cancel" ? { destructiveHint: true } : undefined)
|
|
238
|
+
});
|
|
239
|
+
const inspect = async (capability, context) => {
|
|
240
|
+
await config.authorize(context.input, capability, context);
|
|
241
|
+
const state = config.state.parse(await config.inspect(context.input, context));
|
|
242
|
+
const snapshot = config.snapshot.parse(await config.classify(state, context));
|
|
243
|
+
return { state, snapshot };
|
|
244
|
+
};
|
|
245
|
+
const start = defineRuntimeTool({
|
|
246
|
+
...common("start"),
|
|
247
|
+
identity: identity("start", "POST"),
|
|
248
|
+
input: config.startInput,
|
|
249
|
+
output: config.id,
|
|
250
|
+
handler: (context) => config.start(context.input, context)
|
|
251
|
+
});
|
|
252
|
+
const status = defineRuntimeTool({
|
|
253
|
+
...common("status"),
|
|
254
|
+
identity: identity("status", "GET"),
|
|
255
|
+
input: config.id,
|
|
256
|
+
output: config.snapshot,
|
|
257
|
+
handler: async (context) => (await inspect("status", context)).snapshot
|
|
258
|
+
});
|
|
259
|
+
const wait = defineWaitTool({
|
|
260
|
+
...common("wait"),
|
|
261
|
+
identity: identity("wait", "GET"),
|
|
262
|
+
input: config.id,
|
|
263
|
+
state: config.snapshot,
|
|
264
|
+
poll: async (_id, context) => (await inspect("wait", context)).snapshot,
|
|
265
|
+
done: terminal,
|
|
266
|
+
backoff: config.backoff,
|
|
267
|
+
defaultTimeout: config.defaultTimeout,
|
|
268
|
+
timeoutFromInput: config.timeoutFromId
|
|
269
|
+
});
|
|
270
|
+
const definitions = { start, status, wait };
|
|
271
|
+
const names = new Set;
|
|
272
|
+
if (config.cancel) {
|
|
273
|
+
const capability = config.cancel;
|
|
274
|
+
definitions.cancel = defineRuntimeTool({
|
|
275
|
+
...common("cancel"),
|
|
276
|
+
identity: identity("cancel", "DELETE"),
|
|
277
|
+
input: config.id,
|
|
278
|
+
output: AsyncOperationCancelResultSchema,
|
|
279
|
+
handler: async (context) => {
|
|
280
|
+
const inspected = await inspect("cancel", context);
|
|
281
|
+
return capability.handler(inspected.state, context);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (config.result) {
|
|
286
|
+
const capability = config.result;
|
|
287
|
+
definitions.result = defineRuntimeTool({
|
|
288
|
+
...common("result"),
|
|
289
|
+
identity: identity("result", "GET"),
|
|
290
|
+
input: config.id,
|
|
291
|
+
output: capability.output,
|
|
292
|
+
handler: async (context) => {
|
|
293
|
+
const inspected = await inspect("result", context);
|
|
294
|
+
if (!succeeded(inspected.snapshot)) {
|
|
295
|
+
throw new AppError("OPERATION_NOT_SUCCEEDED", "Operation result is not available", 409);
|
|
296
|
+
}
|
|
297
|
+
return capability.handler(inspected.state, context);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (config.artifacts) {
|
|
302
|
+
const capability = config.artifacts;
|
|
303
|
+
definitions.artifacts = defineRuntimeTool({
|
|
304
|
+
...common("artifacts"),
|
|
305
|
+
identity: identity("artifacts", "GET"),
|
|
306
|
+
input: config.id,
|
|
307
|
+
output: capability.output,
|
|
308
|
+
handler: async (context) => {
|
|
309
|
+
const inspected = await inspect("artifacts", context);
|
|
310
|
+
if (!succeeded(inspected.snapshot)) {
|
|
311
|
+
throw new AppError("OPERATION_NOT_SUCCEEDED", "Operation artifacts are not available", 409);
|
|
312
|
+
}
|
|
313
|
+
return capability.handler(inspected.state, context);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
for (const definition of Object.values(definitions)) {
|
|
318
|
+
assertUniqueToolName(definition.name, names.has(definition.name), "in-process tool name");
|
|
319
|
+
names.add(definition.name);
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
...definitions,
|
|
323
|
+
runtimeTools: Object.values(definitions),
|
|
324
|
+
schemas: {
|
|
325
|
+
id: config.id,
|
|
326
|
+
snapshot: config.snapshot,
|
|
327
|
+
cancelResult: AsyncOperationCancelResultSchema
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function bindContractAsyncOperation(config) {
|
|
332
|
+
const endpoint = (key) => {
|
|
333
|
+
const found = config.contract.endpoints[key];
|
|
334
|
+
if (!found)
|
|
335
|
+
throw new Error(`Contract async operation method "${key}" not found`);
|
|
336
|
+
return found;
|
|
337
|
+
};
|
|
338
|
+
const start = endpoint(config.capabilities.start);
|
|
339
|
+
const status = endpoint(config.capabilities.status);
|
|
340
|
+
const wait = endpoint(config.capabilities.wait);
|
|
341
|
+
if (!start.output || !status.output || !wait.output) {
|
|
342
|
+
throw new Error("Contract async operation requires outputs for start, status and wait");
|
|
343
|
+
}
|
|
344
|
+
if (status.input !== start.output || wait.input !== start.output) {
|
|
345
|
+
const capability = status.input !== start.output ? "status" : "wait";
|
|
346
|
+
throw new Error(`Contract async operation capability "${capability}" input must reuse the same schema instance as the start output`);
|
|
347
|
+
}
|
|
348
|
+
if (wait.output !== status.output) {
|
|
349
|
+
throw new Error('Contract async operation capability "wait" output must reuse the same schema instance as the status output');
|
|
350
|
+
}
|
|
351
|
+
const validateOptional = (capability) => {
|
|
352
|
+
const key = config.capabilities[capability];
|
|
353
|
+
if (key && endpoint(key).input !== start.output) {
|
|
354
|
+
throw new Error(`Contract async operation capability "${capability}" input must reuse the same schema instance as the start output`);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
validateOptional("cancel");
|
|
358
|
+
validateOptional("result");
|
|
359
|
+
validateOptional("artifacts");
|
|
360
|
+
return {
|
|
361
|
+
contract: config.contract,
|
|
362
|
+
handlers: config.handlers,
|
|
363
|
+
capabilities: config.capabilities,
|
|
364
|
+
schemas: { id: start.output, snapshot: status.output }
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// src/tools/download-core.ts
|
|
368
|
+
import { basename, extname } from "node:path";
|
|
369
|
+
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
370
|
+
var MIME_EXT = {
|
|
371
|
+
"image/png": ".png",
|
|
372
|
+
"image/jpeg": ".jpg",
|
|
373
|
+
"image/webp": ".webp",
|
|
374
|
+
"image/gif": ".gif",
|
|
375
|
+
"video/mp4": ".mp4",
|
|
376
|
+
"video/webm": ".webm",
|
|
377
|
+
"audio/mpeg": ".mp3",
|
|
378
|
+
"audio/wav": ".wav",
|
|
379
|
+
"audio/ogg": ".ogg"
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
class DownloadOperationError extends Error {
|
|
383
|
+
code;
|
|
384
|
+
status;
|
|
385
|
+
constructor(code, message, status) {
|
|
386
|
+
super(message);
|
|
387
|
+
this.code = code;
|
|
388
|
+
this.status = status;
|
|
389
|
+
this.name = "DownloadOperationError";
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function extensionFor(url, mime) {
|
|
393
|
+
if (MIME_EXT[mime])
|
|
394
|
+
return MIME_EXT[mime];
|
|
395
|
+
try {
|
|
396
|
+
const ext = extname(new URL(url).pathname).toLowerCase();
|
|
397
|
+
if (ext)
|
|
398
|
+
return ext;
|
|
399
|
+
} catch {}
|
|
400
|
+
return ".bin";
|
|
401
|
+
}
|
|
402
|
+
function baseNameFor(url) {
|
|
403
|
+
try {
|
|
404
|
+
const path = new URL(url).pathname;
|
|
405
|
+
return (basename(path, extname(path)) || "download").slice(0, 60);
|
|
406
|
+
} catch {
|
|
407
|
+
return "download";
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function runDownloadOperation(config) {
|
|
411
|
+
const res = await fetchGuarded(new URL(config.url), config.allowPrivateHosts ?? false, {
|
|
412
|
+
timeoutMs: config.timeoutMs,
|
|
413
|
+
signal: config.signal
|
|
414
|
+
});
|
|
415
|
+
if (!res.ok) {
|
|
416
|
+
throw new DownloadOperationError("DOWNLOAD_HTTP_ERROR", `HTTP ${res.status}`, 502);
|
|
417
|
+
}
|
|
418
|
+
const mimeType = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() || "application/octet-stream";
|
|
419
|
+
const max = config.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
420
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
421
|
+
if (declared > max) {
|
|
422
|
+
await res.body?.cancel();
|
|
423
|
+
throw new DownloadOperationError("DOWNLOAD_TOO_LARGE", `file exceeds the ${max}-byte cap`, 413);
|
|
424
|
+
}
|
|
425
|
+
const path = config.path ?? `${baseNameFor(config.url)}${extensionFor(config.url, mimeType)}`;
|
|
426
|
+
return config.files.write(path, res.body ?? new Uint8Array, {
|
|
427
|
+
maxBytes: max,
|
|
428
|
+
mediaType: mimeType,
|
|
429
|
+
signal: config.signal
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/tools/define-download-tool.ts
|
|
434
|
+
function defineDownloadTool(config) {
|
|
435
|
+
return defineRuntimeTool({
|
|
436
|
+
name: config.name ?? "download",
|
|
437
|
+
description: config.description,
|
|
438
|
+
identity: managedNativeIdentity(config.identity, "POST"),
|
|
439
|
+
input: config.input,
|
|
440
|
+
output: ManagedFileRefSchema,
|
|
441
|
+
transports: config.transports,
|
|
442
|
+
annotations: config.annotations,
|
|
443
|
+
present: config.present,
|
|
444
|
+
handler: async (context) => {
|
|
445
|
+
const url = await config.resolveUrl(context.input, context);
|
|
446
|
+
if (!url) {
|
|
447
|
+
throw new AppError("DOWNLOAD_NOT_FOUND", "Nothing to download", 404);
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
return await runDownloadOperation({
|
|
451
|
+
url,
|
|
452
|
+
files: config.files,
|
|
453
|
+
path: config.pathFromInput?.(context.input),
|
|
454
|
+
allowPrivateHosts: config.allowPrivateHosts,
|
|
455
|
+
maxBytes: config.maxBytes,
|
|
456
|
+
timeoutMs: config.timeoutMs,
|
|
457
|
+
signal: context.signal
|
|
458
|
+
});
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (error instanceof DownloadOperationError) {
|
|
461
|
+
throw new AppError(error.code, `Download failed: ${error.message}`, error.status);
|
|
462
|
+
}
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
// src/tools/define-upload-tool.ts
|
|
469
|
+
import { z as z2 } from "zod";
|
|
470
|
+
|
|
471
|
+
// src/tools/upload-core.ts
|
|
472
|
+
function runUploadOperation(files, path, upload, signal) {
|
|
473
|
+
signal?.throwIfAborted();
|
|
474
|
+
return files.read(path, { signal }).then((source) => upload(source, signal));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/tools/define-upload-tool.ts
|
|
478
|
+
var UploadToolInputSchema = z2.object({
|
|
479
|
+
path: ManagedFilePathSchema.describe("Path relative to the configured file boundary")
|
|
480
|
+
});
|
|
481
|
+
function defineUploadTool(config) {
|
|
482
|
+
return defineRuntimeTool({
|
|
483
|
+
name: config.name ?? "upload",
|
|
484
|
+
description: config.description,
|
|
485
|
+
identity: managedNativeIdentity(config.identity, "POST"),
|
|
486
|
+
input: UploadToolInputSchema,
|
|
487
|
+
output: config.output,
|
|
488
|
+
transports: config.transports,
|
|
489
|
+
annotations: config.annotations,
|
|
490
|
+
present: config.present,
|
|
491
|
+
handler: async (context) => runUploadOperation(config.files, context.input.path, (source) => config.upload(source, context), context.signal)
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
// src/tools/view-file.ts
|
|
495
|
+
import { extname as extname2 } from "node:path";
|
|
496
|
+
import { z as z3 } from "zod";
|
|
497
|
+
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
498
|
+
var MAX_VIEW_FILES = 20;
|
|
499
|
+
var EXT_MIME = {
|
|
500
|
+
".png": "image/png",
|
|
501
|
+
".jpg": "image/jpeg",
|
|
502
|
+
".jpeg": "image/jpeg",
|
|
503
|
+
".gif": "image/gif",
|
|
504
|
+
".webp": "image/webp",
|
|
505
|
+
".mp3": "audio/mpeg",
|
|
506
|
+
".wav": "audio/wav",
|
|
507
|
+
".ogg": "audio/ogg",
|
|
508
|
+
".m4a": "audio/mp4",
|
|
509
|
+
".mp4": "video/mp4",
|
|
510
|
+
".webm": "video/webm",
|
|
511
|
+
".mov": "video/quicktime"
|
|
512
|
+
};
|
|
513
|
+
var McpAnnotationsSchema = z3.object({
|
|
514
|
+
audience: z3.array(z3.enum(["user", "assistant"])).optional(),
|
|
515
|
+
priority: z3.number().optional()
|
|
516
|
+
});
|
|
517
|
+
var McpMediaContentSchema = z3.discriminatedUnion("type", [
|
|
518
|
+
z3.object({ type: z3.literal("text"), text: z3.string() }),
|
|
519
|
+
z3.object({
|
|
520
|
+
type: z3.literal("image"),
|
|
521
|
+
data: z3.string(),
|
|
522
|
+
mimeType: z3.string(),
|
|
523
|
+
annotations: McpAnnotationsSchema.optional()
|
|
524
|
+
}),
|
|
525
|
+
z3.object({
|
|
526
|
+
type: z3.literal("audio"),
|
|
527
|
+
data: z3.string(),
|
|
528
|
+
mimeType: z3.string(),
|
|
529
|
+
annotations: McpAnnotationsSchema.optional()
|
|
530
|
+
})
|
|
531
|
+
]);
|
|
532
|
+
var ViewFileErrorSchema = z3.object({
|
|
533
|
+
path: z3.string(),
|
|
534
|
+
message: z3.string()
|
|
535
|
+
});
|
|
536
|
+
var ViewFileOutputSchema = z3.object({
|
|
537
|
+
content: z3.array(McpMediaContentSchema),
|
|
538
|
+
errors: z3.array(ViewFileErrorSchema)
|
|
539
|
+
});
|
|
540
|
+
async function fetchSource(pathOrUrl, options, maxBytes) {
|
|
541
|
+
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
542
|
+
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
543
|
+
const url = new URL(pathOrUrl);
|
|
544
|
+
const res = await fetchGuarded(url, options.allowPrivateHosts ?? false, {
|
|
545
|
+
timeoutMs: options.timeoutMs,
|
|
546
|
+
signal: options.signal
|
|
547
|
+
});
|
|
548
|
+
if (!res.ok)
|
|
549
|
+
throw new Error(`HTTP ${res.status}`);
|
|
550
|
+
const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
551
|
+
const mimeType = headerMime || extMime || "application/octet-stream";
|
|
552
|
+
if (mimeType.startsWith("video/")) {
|
|
553
|
+
await res.body?.cancel();
|
|
554
|
+
return { video: true, mimeType, bytesRead: 0 };
|
|
555
|
+
}
|
|
556
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
557
|
+
if (declared > maxBytes) {
|
|
558
|
+
await res.body?.cancel();
|
|
559
|
+
return { tooLarge: true, mimeType, bytesRead: 0 };
|
|
560
|
+
}
|
|
561
|
+
const buffer = await readCapped(res, maxBytes);
|
|
562
|
+
return buffer ? { buffer, mimeType, bytesRead: buffer.length } : { tooLarge: true, mimeType, bytesRead: maxBytes };
|
|
563
|
+
}
|
|
564
|
+
if (!options.files) {
|
|
565
|
+
throw new Error("local file paths are disabled — set files to allow them");
|
|
566
|
+
}
|
|
567
|
+
options.signal?.throwIfAborted();
|
|
568
|
+
if (!extMime) {
|
|
569
|
+
throw new Error("refusing to read a non-media file");
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const source = await options.files.read(pathOrUrl, {
|
|
573
|
+
maxBytes,
|
|
574
|
+
signal: options.signal
|
|
575
|
+
});
|
|
576
|
+
const buffer = Buffer.from(source.bytes);
|
|
577
|
+
return {
|
|
578
|
+
buffer,
|
|
579
|
+
mimeType: source.ref.mediaType ?? extMime,
|
|
580
|
+
bytesRead: buffer.length
|
|
581
|
+
};
|
|
582
|
+
} catch (error) {
|
|
583
|
+
if (error instanceof ManagedFileError && error.code === "FILE_TOO_LARGE") {
|
|
584
|
+
return { tooLarge: true, mimeType: extMime, bytesRead: maxBytes };
|
|
585
|
+
}
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function resolveMediaWithinBudget(pathOrUrl, options, maxBytes) {
|
|
590
|
+
if (maxBytes <= 0) {
|
|
591
|
+
return {
|
|
592
|
+
content: [
|
|
593
|
+
{ type: "text", text: `[media] total inline budget exhausted — ${pathOrUrl}` }
|
|
594
|
+
],
|
|
595
|
+
bytes: 0
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
599
|
+
if (extMime?.startsWith("video/")) {
|
|
600
|
+
return {
|
|
601
|
+
content: [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }],
|
|
602
|
+
bytes: 0
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const result = await fetchSource(pathOrUrl, options, maxBytes);
|
|
606
|
+
if ("video" in result) {
|
|
607
|
+
return {
|
|
608
|
+
content: [{ type: "text", text: `[video] ${result.mimeType} — ${pathOrUrl}` }],
|
|
609
|
+
bytes: result.bytesRead
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
if ("tooLarge" in result) {
|
|
613
|
+
return {
|
|
614
|
+
content: [
|
|
615
|
+
{ type: "text", text: `[${result.mimeType}] too large to inline — ${pathOrUrl}` }
|
|
616
|
+
],
|
|
617
|
+
bytes: result.bytesRead
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const { buffer, mimeType } = result;
|
|
621
|
+
const sizeKb = (buffer.length / 1024).toFixed(0);
|
|
622
|
+
if (mimeType.startsWith("image/")) {
|
|
623
|
+
return {
|
|
624
|
+
content: [
|
|
625
|
+
{
|
|
626
|
+
type: "image",
|
|
627
|
+
data: buffer.toString("base64"),
|
|
628
|
+
mimeType,
|
|
629
|
+
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
630
|
+
},
|
|
631
|
+
{ type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
|
|
632
|
+
],
|
|
633
|
+
bytes: buffer.length
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
if (mimeType.startsWith("audio/")) {
|
|
637
|
+
return {
|
|
638
|
+
content: [
|
|
639
|
+
{
|
|
640
|
+
type: "audio",
|
|
641
|
+
data: buffer.toString("base64"),
|
|
642
|
+
mimeType,
|
|
643
|
+
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
644
|
+
},
|
|
645
|
+
{ type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
|
|
646
|
+
],
|
|
647
|
+
bytes: buffer.length
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
content: [{ type: "text", text: `[${mimeType}] ${sizeKb}KB — ${pathOrUrl}` }],
|
|
652
|
+
bytes: buffer.length
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
async function resolveMedia(pathOrUrl, options = {}) {
|
|
656
|
+
return (await resolveMediaWithinBudget(pathOrUrl, options, MAX_INLINE_BYTES)).content;
|
|
657
|
+
}
|
|
658
|
+
var ViewFileInputSchema = z3.object({
|
|
659
|
+
paths: z3.union([z3.string(), z3.array(z3.string()).max(MAX_VIEW_FILES)]).describe("Media URL(s) or file path(s) to view")
|
|
660
|
+
});
|
|
661
|
+
async function runViewFileOperation(paths, options = {}, signal) {
|
|
662
|
+
const list = Array.isArray(paths) ? paths : [paths];
|
|
663
|
+
const content = [];
|
|
664
|
+
const errors = [];
|
|
665
|
+
let remainingBytes = MAX_INLINE_BYTES;
|
|
666
|
+
for (const pathOrUrl of list) {
|
|
667
|
+
signal?.throwIfAborted();
|
|
668
|
+
try {
|
|
669
|
+
const resolved = await resolveMediaWithinBudget(pathOrUrl, { ...options, signal }, remainingBytes);
|
|
670
|
+
remainingBytes -= resolved.bytes;
|
|
671
|
+
content.push(...resolved.content);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
if (signal?.aborted)
|
|
674
|
+
throw signal.reason ?? error;
|
|
675
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
676
|
+
errors.push({ path: pathOrUrl, message });
|
|
677
|
+
content.push({ type: "text", text: `[${pathOrUrl}] Error: ${message}` });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return ViewFileOutputSchema.parse({ content, errors });
|
|
681
|
+
}
|
|
682
|
+
function mountViewFile(server, options = {}) {
|
|
683
|
+
server.registerTool("view_file", {
|
|
684
|
+
description: "View media (image, audio, video) by URL or local path — returns it as content you can SEE / HEAR. Pass several paths to view multiple files at once. Use it on a generation `output` url to inspect the result.",
|
|
685
|
+
inputSchema: ViewFileInputSchema.shape,
|
|
686
|
+
annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
|
|
687
|
+
}, async (args) => {
|
|
688
|
+
const result = await runViewFileOperation(args.paths, options);
|
|
689
|
+
return { content: result.content };
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/tools/define-view-file-tool.ts
|
|
694
|
+
function defineViewFileTool(config) {
|
|
695
|
+
const defaults = {
|
|
696
|
+
mcp: (output) => ({ content: output.content }),
|
|
697
|
+
agent: (output) => ({
|
|
698
|
+
type: "content",
|
|
699
|
+
value: output.content.map((part) => {
|
|
700
|
+
if (part.type === "text")
|
|
701
|
+
return part;
|
|
702
|
+
return {
|
|
703
|
+
type: "file",
|
|
704
|
+
data: { type: "data", data: part.data },
|
|
705
|
+
mediaType: part.mimeType
|
|
706
|
+
};
|
|
707
|
+
})
|
|
708
|
+
})
|
|
709
|
+
};
|
|
710
|
+
return defineRuntimeTool({
|
|
711
|
+
name: config.name ?? "view_file",
|
|
712
|
+
description: config.description,
|
|
713
|
+
identity: managedNativeIdentity(config.identity, "GET"),
|
|
714
|
+
input: ViewFileInputSchema,
|
|
715
|
+
output: ViewFileOutputSchema,
|
|
716
|
+
transports: config.transports,
|
|
717
|
+
annotations: config.annotations ?? {
|
|
718
|
+
title: "View Media",
|
|
719
|
+
readOnlyHint: true,
|
|
720
|
+
idempotentHint: true
|
|
721
|
+
},
|
|
722
|
+
present: { ...defaults, ...config.present },
|
|
723
|
+
handler: (context) => runViewFileOperation(context.input.paths, {
|
|
724
|
+
files: config.files,
|
|
725
|
+
allowPrivateHosts: config.allowPrivateHosts,
|
|
726
|
+
timeoutMs: config.timeoutMs
|
|
727
|
+
}, context.signal)
|
|
728
|
+
});
|
|
729
|
+
}
|
|
265
730
|
// src/tools/invoker.ts
|
|
266
731
|
function createToolInvoker(services, config) {
|
|
267
732
|
const serviceList = Array.isArray(services) ? services : [services];
|
|
@@ -309,6 +774,24 @@ function createToolInvoker(services, config) {
|
|
|
309
774
|
}
|
|
310
775
|
});
|
|
311
776
|
}
|
|
777
|
+
// src/tools/lifecycle.ts
|
|
778
|
+
function composeToolLifecycle(...hooks) {
|
|
779
|
+
return {
|
|
780
|
+
beforeHandle: async (ctx, endpoint) => {
|
|
781
|
+
for (const hook of hooks)
|
|
782
|
+
await hook?.beforeHandle?.(ctx, endpoint);
|
|
783
|
+
},
|
|
784
|
+
afterHandle: async (ctx, result, endpoint) => {
|
|
785
|
+
let current = result;
|
|
786
|
+
for (const hook of hooks) {
|
|
787
|
+
const transformed = await hook?.afterHandle?.(ctx, current, endpoint);
|
|
788
|
+
if (transformed !== undefined)
|
|
789
|
+
current = transformed;
|
|
790
|
+
}
|
|
791
|
+
return current;
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
}
|
|
312
795
|
// src/tools/list-names.ts
|
|
313
796
|
var TOOL_TRANSPORTS = ["MCP", "AGENT", "CLI"];
|
|
314
797
|
function listToolNames(surface) {
|
|
@@ -358,7 +841,7 @@ import {
|
|
|
358
841
|
createRequestStateCodec,
|
|
359
842
|
McpServer
|
|
360
843
|
} from "@modelcontextprotocol/server";
|
|
361
|
-
import { z as
|
|
844
|
+
import { z as z5 } from "zod";
|
|
362
845
|
|
|
363
846
|
// src/tools/mcp-app.ts
|
|
364
847
|
import { readFileSync } from "node:fs";
|
|
@@ -975,13 +1458,13 @@ function runInMcpRequestContext(context, toolName, body) {
|
|
|
975
1458
|
import {
|
|
976
1459
|
isCallToolResult
|
|
977
1460
|
} from "@modelcontextprotocol/server";
|
|
978
|
-
import { z } from "zod";
|
|
1461
|
+
import { z as z4 } from "zod";
|
|
979
1462
|
function mountPreparedRuntimeMcp(server, tools, config) {
|
|
980
1463
|
for (const { definition, descriptor } of tools) {
|
|
981
1464
|
const { mountable } = descriptor;
|
|
982
1465
|
const toolConfig = {
|
|
983
1466
|
description: definition.description,
|
|
984
|
-
inputSchema:
|
|
1467
|
+
inputSchema: z4.looseObject({}).meta(presentationMetadata(descriptor.inputSchema))
|
|
985
1468
|
};
|
|
986
1469
|
if (descriptor.outputSchema)
|
|
987
1470
|
toolConfig.outputSchema = descriptor.outputSchema;
|
|
@@ -1070,7 +1553,7 @@ function mountPreparedMcp(mcpServer, prepared, config = {}) {
|
|
|
1070
1553
|
const { mountable } = descriptor;
|
|
1071
1554
|
const toolConfig = {
|
|
1072
1555
|
description: mountable.method.desc,
|
|
1073
|
-
inputSchema:
|
|
1556
|
+
inputSchema: z5.looseObject({}).meta(presentationMetadata(descriptor.inputSchema))
|
|
1074
1557
|
};
|
|
1075
1558
|
if (descriptor.outputSchema)
|
|
1076
1559
|
toolConfig.outputSchema = descriptor.outputSchema;
|
|
@@ -1409,9 +1892,103 @@ async function createStdioMcpServer(config) {
|
|
|
1409
1892
|
maxSubscriptions: 0
|
|
1410
1893
|
});
|
|
1411
1894
|
}
|
|
1895
|
+
// src/tools/mcp-stdio-signals.ts
|
|
1896
|
+
var bound = new WeakSet;
|
|
1897
|
+
function bindStdioProcessSignals(handle, options = {}) {
|
|
1898
|
+
if (bound.has(handle)) {
|
|
1899
|
+
throw new Error("[stitchkit] bindStdioProcessSignals: this stdio handle is already bound; close the first binding before creating another");
|
|
1900
|
+
}
|
|
1901
|
+
bound.add(handle);
|
|
1902
|
+
const signals = [...new Set(options.signals ?? DEFAULT_PROCESS_SIGNALS)];
|
|
1903
|
+
const source = options.signalSource ?? defaultSignalSource;
|
|
1904
|
+
const handlers = [];
|
|
1905
|
+
let started = false;
|
|
1906
|
+
let sameTurnAsStart = false;
|
|
1907
|
+
let closed = false;
|
|
1908
|
+
let resolveChain = () => {
|
|
1909
|
+
return;
|
|
1910
|
+
};
|
|
1911
|
+
let rejectChain = () => {
|
|
1912
|
+
return;
|
|
1913
|
+
};
|
|
1914
|
+
const promise = new Promise((resolve, reject) => {
|
|
1915
|
+
resolveChain = resolve;
|
|
1916
|
+
rejectChain = reject;
|
|
1917
|
+
});
|
|
1918
|
+
promise.catch(() => {
|
|
1919
|
+
return;
|
|
1920
|
+
});
|
|
1921
|
+
const removeListeners = () => {
|
|
1922
|
+
if (closed)
|
|
1923
|
+
return;
|
|
1924
|
+
closed = true;
|
|
1925
|
+
for (const [signal, handler] of handlers)
|
|
1926
|
+
source.off(signal, handler);
|
|
1927
|
+
if (!started)
|
|
1928
|
+
bound.delete(handle);
|
|
1929
|
+
};
|
|
1930
|
+
const close = () => {
|
|
1931
|
+
const hadStarted = started;
|
|
1932
|
+
removeListeners();
|
|
1933
|
+
if (!hadStarted)
|
|
1934
|
+
resolveChain();
|
|
1935
|
+
};
|
|
1936
|
+
const run = async (signal) => {
|
|
1937
|
+
try {
|
|
1938
|
+
await options.onClose?.(signal);
|
|
1939
|
+
} catch (error) {
|
|
1940
|
+
reportSignalError("prepare", error, options.onError);
|
|
1941
|
+
}
|
|
1942
|
+
try {
|
|
1943
|
+
await handle.close();
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
rejectChain(error);
|
|
1946
|
+
reportSignalError("close", error, options.onError);
|
|
1947
|
+
removeListeners();
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
resolveChain();
|
|
1951
|
+
try {
|
|
1952
|
+
await options.onComplete?.();
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
reportSignalError("complete", error, options.onError);
|
|
1955
|
+
} finally {
|
|
1956
|
+
removeListeners();
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
const onSignal = (signal) => {
|
|
1960
|
+
if (!started) {
|
|
1961
|
+
started = true;
|
|
1962
|
+
sameTurnAsStart = true;
|
|
1963
|
+
queueMicrotask(() => {
|
|
1964
|
+
sameTurnAsStart = false;
|
|
1965
|
+
});
|
|
1966
|
+
run(signal).catch((error) => reportSignalError("close", error, options.onError));
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
if (sameTurnAsStart)
|
|
1970
|
+
return;
|
|
1971
|
+
removeListeners();
|
|
1972
|
+
guardSignalCallback(() => options.onRepeatedSignal?.(signal), "close", options.onError);
|
|
1973
|
+
let restored = false;
|
|
1974
|
+
try {
|
|
1975
|
+
restored = source.raiseDefault(signal);
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
reportSignalError("close", error, options.onError);
|
|
1978
|
+
}
|
|
1979
|
+
if (!restored) {
|
|
1980
|
+
guardSignalCallback(() => options.onEscalationBlocked?.(signal), "close", options.onError);
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
for (const signal of signals) {
|
|
1984
|
+
const handler = () => onSignal(signal);
|
|
1985
|
+
handlers.push([signal, handler]);
|
|
1986
|
+
source.on(signal, handler);
|
|
1987
|
+
}
|
|
1988
|
+
return { promise, close };
|
|
1989
|
+
}
|
|
1412
1990
|
// src/tools/mount-download.ts
|
|
1413
|
-
import {
|
|
1414
|
-
import { z as z3 } from "zod";
|
|
1991
|
+
import { z as z6 } from "zod";
|
|
1415
1992
|
|
|
1416
1993
|
// src/tools/native-result.ts
|
|
1417
1994
|
function textResult(text, isError = false) {
|
|
@@ -1420,84 +1997,45 @@ function textResult(text, isError = false) {
|
|
|
1420
1997
|
}
|
|
1421
1998
|
|
|
1422
1999
|
// src/tools/mount-download.ts
|
|
1423
|
-
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
1424
|
-
var MIME_EXT = {
|
|
1425
|
-
"image/png": ".png",
|
|
1426
|
-
"image/jpeg": ".jpg",
|
|
1427
|
-
"image/webp": ".webp",
|
|
1428
|
-
"image/gif": ".gif",
|
|
1429
|
-
"video/mp4": ".mp4",
|
|
1430
|
-
"video/webm": ".webm",
|
|
1431
|
-
"audio/mpeg": ".mp3",
|
|
1432
|
-
"audio/wav": ".wav",
|
|
1433
|
-
"audio/ogg": ".ogg"
|
|
1434
|
-
};
|
|
1435
|
-
function extensionFor(url, mime) {
|
|
1436
|
-
if (MIME_EXT[mime])
|
|
1437
|
-
return MIME_EXT[mime];
|
|
1438
|
-
try {
|
|
1439
|
-
const ext = extname(new URL(url).pathname).toLowerCase();
|
|
1440
|
-
if (ext)
|
|
1441
|
-
return ext;
|
|
1442
|
-
} catch {}
|
|
1443
|
-
return ".bin";
|
|
1444
|
-
}
|
|
1445
|
-
function baseNameFor(url) {
|
|
1446
|
-
try {
|
|
1447
|
-
const path = new URL(url).pathname;
|
|
1448
|
-
return (basename(path, extname(path)) || "download").slice(0, 60);
|
|
1449
|
-
} catch {
|
|
1450
|
-
return "download";
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
2000
|
function mountDownload(server, config) {
|
|
1454
2001
|
const name = config.name ?? "download";
|
|
1455
2002
|
assertToolName(name, "<native>", "download");
|
|
1456
|
-
server.registerTool(name, { description: config.description, inputSchema:
|
|
2003
|
+
server.registerTool(name, { description: config.description, inputSchema: z6.object(config.inputSchema) }, async (rawArgs) => {
|
|
1457
2004
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
1458
2005
|
try {
|
|
1459
2006
|
const url = await config.resolveUrl(args);
|
|
1460
2007
|
if (!url)
|
|
1461
2008
|
return textResult("Nothing to download.", true);
|
|
1462
|
-
const
|
|
2009
|
+
const result = await runDownloadOperation({
|
|
2010
|
+
url,
|
|
2011
|
+
files: config.files,
|
|
2012
|
+
path: config.pathFromArgs?.(args),
|
|
2013
|
+
allowPrivateHosts: config.allowPrivateHosts,
|
|
2014
|
+
maxBytes: config.maxBytes,
|
|
1463
2015
|
timeoutMs: config.timeoutMs
|
|
1464
2016
|
});
|
|
1465
|
-
|
|
1466
|
-
return textResult(`Download failed: HTTP ${res.status}`, true);
|
|
1467
|
-
const mimeType = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() || "application/octet-stream";
|
|
1468
|
-
const max = config.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
1469
|
-
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
1470
|
-
if (declared > max) {
|
|
1471
|
-
await res.body?.cancel();
|
|
1472
|
-
return textResult(`Download failed: file exceeds the ${max}-byte cap`, true);
|
|
1473
|
-
}
|
|
1474
|
-
const buffer = await readCapped(res, max);
|
|
1475
|
-
if (!buffer)
|
|
1476
|
-
return textResult(`Download failed: file exceeds the ${max}-byte cap`, true);
|
|
1477
|
-
const dir = resolve(config.dirFromArgs?.(args) ?? config.defaultDir);
|
|
1478
|
-
const filePath = resolve(dir, `${baseNameFor(url)}${extensionFor(url, mimeType)}`);
|
|
1479
|
-
await writeDownload(dir, filePath, buffer);
|
|
1480
|
-
return textResult(JSON.stringify({ path: filePath, size: buffer.length, mimeType }, null, 2));
|
|
2017
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1481
2018
|
} catch (err) {
|
|
1482
2019
|
return textResult(`Download failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
1483
2020
|
}
|
|
1484
2021
|
});
|
|
1485
2022
|
}
|
|
1486
2023
|
// src/tools/mount-upload.ts
|
|
1487
|
-
import { z as z4 } from "zod";
|
|
1488
2024
|
function mountUpload(server, config) {
|
|
1489
2025
|
const name = config.name ?? "upload";
|
|
1490
2026
|
assertToolName(name, "<native>", "upload");
|
|
1491
2027
|
server.registerTool(name, {
|
|
1492
2028
|
description: config.description,
|
|
1493
|
-
inputSchema: {
|
|
2029
|
+
inputSchema: {
|
|
2030
|
+
path: ManagedFilePathSchema.describe("Path relative to the configured file boundary")
|
|
2031
|
+
}
|
|
1494
2032
|
}, async (rawArgs) => {
|
|
1495
2033
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
1496
2034
|
const path = typeof args.path === "string" ? args.path : "";
|
|
1497
2035
|
if (!path)
|
|
1498
2036
|
return textResult("Provide `path`.", true);
|
|
1499
2037
|
try {
|
|
1500
|
-
const uploaded = await config.upload
|
|
2038
|
+
const uploaded = await runUploadOperation(config.files, path, config.upload);
|
|
1501
2039
|
return textResult(JSON.stringify(uploaded ?? null, null, 2));
|
|
1502
2040
|
} catch (err) {
|
|
1503
2041
|
return textResult(`Upload failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
@@ -1505,15 +2043,16 @@ function mountUpload(server, config) {
|
|
|
1505
2043
|
});
|
|
1506
2044
|
}
|
|
1507
2045
|
// src/tools/mount-wait.ts
|
|
1508
|
-
import { z as
|
|
2046
|
+
import { z as z7 } from "zod";
|
|
1509
2047
|
function mountWait(server, config) {
|
|
1510
2048
|
const name = config.name ?? "wait";
|
|
1511
2049
|
assertToolName(name, "<native>", "wait");
|
|
1512
|
-
server.registerTool(name, { description: config.description, inputSchema:
|
|
2050
|
+
server.registerTool(name, { description: config.description, inputSchema: z7.object(config.inputSchema) }, async (rawArgs) => {
|
|
1513
2051
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
1514
2052
|
try {
|
|
1515
|
-
const { state, timedOut } = await
|
|
1516
|
-
|
|
2053
|
+
const { state, timedOut } = await runWaitOperation({
|
|
2054
|
+
input: args,
|
|
2055
|
+
poll: (input) => config.poll(input),
|
|
1517
2056
|
done: config.done,
|
|
1518
2057
|
backoff: config.backoff,
|
|
1519
2058
|
timeoutSec: config.timeoutFromArgs?.(args) ?? config.defaultTimeout
|
|
@@ -1527,19 +2066,19 @@ function mountWait(server, config) {
|
|
|
1527
2066
|
}
|
|
1528
2067
|
// src/tools/oauth-provider.ts
|
|
1529
2068
|
import { randomUUID } from "node:crypto";
|
|
1530
|
-
import { z as
|
|
2069
|
+
import { z as z8 } from "zod";
|
|
1531
2070
|
function isPlainDisplayCharacter(character) {
|
|
1532
2071
|
const codePoint = character.codePointAt(0);
|
|
1533
2072
|
return codePoint !== undefined && codePoint >= 32 && codePoint !== 127 && character !== "<" && character !== ">";
|
|
1534
2073
|
}
|
|
1535
|
-
var CimdClientMetadataSchema =
|
|
1536
|
-
client_id:
|
|
1537
|
-
redirect_uris:
|
|
1538
|
-
client_name:
|
|
2074
|
+
var CimdClientMetadataSchema = z8.object({
|
|
2075
|
+
client_id: z8.url(),
|
|
2076
|
+
redirect_uris: z8.array(z8.url()).min(1),
|
|
2077
|
+
client_name: z8.string().trim().min(1).max(200).refine((value) => Array.from(value).every(isPlainDisplayCharacter), {
|
|
1539
2078
|
message: "client_name must be plain display text"
|
|
1540
2079
|
}),
|
|
1541
|
-
token_endpoint_auth_method:
|
|
1542
|
-
application_type:
|
|
2080
|
+
token_endpoint_auth_method: z8.literal("none"),
|
|
2081
|
+
application_type: z8.enum(["native", "web"]).optional()
|
|
1543
2082
|
});
|
|
1544
2083
|
function createSecureClientMetadataFetcher(policy = {}) {
|
|
1545
2084
|
assertPositiveInteger("cimd.maxBytes", policy.maxBytes ?? DEFAULT_CIMD_MAX_BYTES);
|
|
@@ -2172,67 +2711,6 @@ function mountOAuthProvider(config) {
|
|
|
2172
2711
|
};
|
|
2173
2712
|
return dcrEnabled ? [metadataRoute, registerRoute, authorizeRoute, tokenRoute] : [metadataRoute, authorizeRoute, tokenRoute];
|
|
2174
2713
|
}
|
|
2175
|
-
// src/tools/remote.ts
|
|
2176
|
-
var HTTP_ONLY = Object.freeze(["HTTP"]);
|
|
2177
|
-
function toArgs(ctx) {
|
|
2178
|
-
const { params, input } = ctx;
|
|
2179
|
-
return {
|
|
2180
|
-
...isRecord(params) ? params : {},
|
|
2181
|
-
...isRecord(input) ? input : {}
|
|
2182
|
-
};
|
|
2183
|
-
}
|
|
2184
|
-
function implementRemote(contract, http, options) {
|
|
2185
|
-
const client = createClient(contract, http);
|
|
2186
|
-
const groupScope = contract.meta.scope ?? "public";
|
|
2187
|
-
const methods = {};
|
|
2188
|
-
for (const [key, endpoint] of Object.entries(contract.endpoints)) {
|
|
2189
|
-
methods[key] = {
|
|
2190
|
-
method: endpoint.method,
|
|
2191
|
-
path: endpoint.path,
|
|
2192
|
-
desc: endpoint.desc,
|
|
2193
|
-
serviceName: contract.meta.prefix,
|
|
2194
|
-
key,
|
|
2195
|
-
toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
|
|
2196
|
-
expose: endpoint.rawResponse || endpoint.rawBody || endpoint.responseMeta ? HTTP_ONLY : endpoint.expose,
|
|
2197
|
-
ui: "ui" in endpoint ? endpoint.ui : undefined,
|
|
2198
|
-
annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
|
|
2199
|
-
meta: mergeMeta(contract.meta.meta, endpoint.meta),
|
|
2200
|
-
scope: endpoint.scope ?? groupScope,
|
|
2201
|
-
paramsSchema: endpoint.params,
|
|
2202
|
-
inputSchema: endpoint.input,
|
|
2203
|
-
outputSchema: endpoint.output,
|
|
2204
|
-
multipart: endpoint.multipart,
|
|
2205
|
-
maxJsonBodyBytes: endpoint.maxJsonBodyBytes,
|
|
2206
|
-
idempotent: endpoint.idempotent,
|
|
2207
|
-
rawResponse: endpoint.rawResponse,
|
|
2208
|
-
rawBody: endpoint.rawBody,
|
|
2209
|
-
responseMeta: endpoint.responseMeta,
|
|
2210
|
-
contentType: "contentType" in endpoint ? endpoint.contentType : undefined,
|
|
2211
|
-
handler: async (ctx) => {
|
|
2212
|
-
const call = client[key];
|
|
2213
|
-
if (!call) {
|
|
2214
|
-
throw new Error(`implementRemote: endpoint "${key}" is not exposed over HTTP`);
|
|
2215
|
-
}
|
|
2216
|
-
const args = toArgs(ctx);
|
|
2217
|
-
try {
|
|
2218
|
-
const finalArgs = options?.transformArgs ? await options.transformArgs(key, args) : args;
|
|
2219
|
-
return await call(finalArgs);
|
|
2220
|
-
} catch (err) {
|
|
2221
|
-
if (ApiError.is(err)) {
|
|
2222
|
-
throw new AppError(err.code, err.message, err.status, isRecord(err.details) ? err.details : undefined, err.hint, err.traceId);
|
|
2223
|
-
}
|
|
2224
|
-
throw err;
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
};
|
|
2228
|
-
}
|
|
2229
|
-
return {
|
|
2230
|
-
name: contract.meta.prefix,
|
|
2231
|
-
prefix: contract.meta.prefix,
|
|
2232
|
-
scope: groupScope,
|
|
2233
|
-
methods
|
|
2234
|
-
};
|
|
2235
|
-
}
|
|
2236
2714
|
// src/tools/tool-logger.ts
|
|
2237
2715
|
function createToolLogger(config = {}) {
|
|
2238
2716
|
const log = config.log ?? ((line) => console.error(line));
|
|
@@ -2332,206 +2810,60 @@ function summarizeTransports(surface) {
|
|
|
2332
2810
|
sources
|
|
2333
2811
|
};
|
|
2334
2812
|
}
|
|
2335
|
-
// src/tools/view-file.ts
|
|
2336
|
-
import { readFile, stat } from "node:fs/promises";
|
|
2337
|
-
import { extname as extname2, resolve as resolve2 } from "node:path";
|
|
2338
|
-
import { z as z7 } from "zod";
|
|
2339
|
-
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
2340
|
-
var MAX_VIEW_FILES = 20;
|
|
2341
|
-
var EXT_MIME = {
|
|
2342
|
-
".png": "image/png",
|
|
2343
|
-
".jpg": "image/jpeg",
|
|
2344
|
-
".jpeg": "image/jpeg",
|
|
2345
|
-
".gif": "image/gif",
|
|
2346
|
-
".webp": "image/webp",
|
|
2347
|
-
".mp3": "audio/mpeg",
|
|
2348
|
-
".wav": "audio/wav",
|
|
2349
|
-
".ogg": "audio/ogg",
|
|
2350
|
-
".m4a": "audio/mp4",
|
|
2351
|
-
".mp4": "video/mp4",
|
|
2352
|
-
".webm": "video/webm",
|
|
2353
|
-
".mov": "video/quicktime"
|
|
2354
|
-
};
|
|
2355
|
-
async function fetchSource(pathOrUrl, options, maxBytes) {
|
|
2356
|
-
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
2357
|
-
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
2358
|
-
const url = new URL(pathOrUrl);
|
|
2359
|
-
const res = await fetchGuarded(url, options.allowPrivateHosts ?? false, {
|
|
2360
|
-
timeoutMs: options.timeoutMs
|
|
2361
|
-
});
|
|
2362
|
-
if (!res.ok)
|
|
2363
|
-
throw new Error(`HTTP ${res.status}`);
|
|
2364
|
-
const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
2365
|
-
const mimeType = headerMime || extMime || "application/octet-stream";
|
|
2366
|
-
if (mimeType.startsWith("video/")) {
|
|
2367
|
-
await res.body?.cancel();
|
|
2368
|
-
return { video: true, mimeType, bytesRead: 0 };
|
|
2369
|
-
}
|
|
2370
|
-
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
2371
|
-
if (declared > maxBytes) {
|
|
2372
|
-
await res.body?.cancel();
|
|
2373
|
-
return { tooLarge: true, mimeType, bytesRead: 0 };
|
|
2374
|
-
}
|
|
2375
|
-
const buffer2 = await readCapped(res, maxBytes);
|
|
2376
|
-
return buffer2 ? { buffer: buffer2, mimeType, bytesRead: buffer2.length } : { tooLarge: true, mimeType, bytesRead: maxBytes };
|
|
2377
|
-
}
|
|
2378
|
-
if (!options.baseDir) {
|
|
2379
|
-
throw new Error("local file paths are disabled — set baseDir to allow them");
|
|
2380
|
-
}
|
|
2381
|
-
if (!extMime) {
|
|
2382
|
-
throw new Error("refusing to read a non-media file");
|
|
2383
|
-
}
|
|
2384
|
-
const root = resolve2(options.baseDir);
|
|
2385
|
-
const target = resolve2(root, pathOrUrl);
|
|
2386
|
-
if (!isWithinDir(root, target)) {
|
|
2387
|
-
throw new Error("path escapes the allowed directory");
|
|
2388
|
-
}
|
|
2389
|
-
const realTarget = await realPathWithinDir(root, target);
|
|
2390
|
-
if (realTarget === null) {
|
|
2391
|
-
throw new Error("path escapes the allowed directory");
|
|
2392
|
-
}
|
|
2393
|
-
const info = await stat(realTarget).catch(() => null);
|
|
2394
|
-
if (!info?.isFile())
|
|
2395
|
-
throw new Error("file not found");
|
|
2396
|
-
if (info.size > maxBytes)
|
|
2397
|
-
return { tooLarge: true, mimeType: extMime, bytesRead: 0 };
|
|
2398
|
-
const buffer = await readFile(realTarget);
|
|
2399
|
-
return { buffer, mimeType: extMime, bytesRead: buffer.length };
|
|
2400
|
-
}
|
|
2401
|
-
async function resolveMediaWithinBudget(pathOrUrl, options, maxBytes) {
|
|
2402
|
-
if (maxBytes <= 0) {
|
|
2403
|
-
return {
|
|
2404
|
-
content: [
|
|
2405
|
-
{ type: "text", text: `[media] total inline budget exhausted — ${pathOrUrl}` }
|
|
2406
|
-
],
|
|
2407
|
-
bytes: 0
|
|
2408
|
-
};
|
|
2409
|
-
}
|
|
2410
|
-
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
2411
|
-
if (extMime?.startsWith("video/")) {
|
|
2412
|
-
return {
|
|
2413
|
-
content: [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }],
|
|
2414
|
-
bytes: 0
|
|
2415
|
-
};
|
|
2416
|
-
}
|
|
2417
|
-
const result = await fetchSource(pathOrUrl, options, maxBytes);
|
|
2418
|
-
if ("video" in result) {
|
|
2419
|
-
return {
|
|
2420
|
-
content: [{ type: "text", text: `[video] ${result.mimeType} — ${pathOrUrl}` }],
|
|
2421
|
-
bytes: result.bytesRead
|
|
2422
|
-
};
|
|
2423
|
-
}
|
|
2424
|
-
if ("tooLarge" in result) {
|
|
2425
|
-
return {
|
|
2426
|
-
content: [
|
|
2427
|
-
{ type: "text", text: `[${result.mimeType}] too large to inline — ${pathOrUrl}` }
|
|
2428
|
-
],
|
|
2429
|
-
bytes: result.bytesRead
|
|
2430
|
-
};
|
|
2431
|
-
}
|
|
2432
|
-
const { buffer, mimeType } = result;
|
|
2433
|
-
const sizeKb = (buffer.length / 1024).toFixed(0);
|
|
2434
|
-
if (mimeType.startsWith("image/")) {
|
|
2435
|
-
return {
|
|
2436
|
-
content: [
|
|
2437
|
-
{
|
|
2438
|
-
type: "image",
|
|
2439
|
-
data: buffer.toString("base64"),
|
|
2440
|
-
mimeType,
|
|
2441
|
-
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
2442
|
-
},
|
|
2443
|
-
{ type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
|
|
2444
|
-
],
|
|
2445
|
-
bytes: buffer.length
|
|
2446
|
-
};
|
|
2447
|
-
}
|
|
2448
|
-
if (mimeType.startsWith("audio/")) {
|
|
2449
|
-
return {
|
|
2450
|
-
content: [
|
|
2451
|
-
{
|
|
2452
|
-
type: "audio",
|
|
2453
|
-
data: buffer.toString("base64"),
|
|
2454
|
-
mimeType,
|
|
2455
|
-
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
2456
|
-
},
|
|
2457
|
-
{ type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
|
|
2458
|
-
],
|
|
2459
|
-
bytes: buffer.length
|
|
2460
|
-
};
|
|
2461
|
-
}
|
|
2462
|
-
return {
|
|
2463
|
-
content: [{ type: "text", text: `[${mimeType}] ${sizeKb}KB — ${pathOrUrl}` }],
|
|
2464
|
-
bytes: buffer.length
|
|
2465
|
-
};
|
|
2466
|
-
}
|
|
2467
|
-
async function resolveMedia(pathOrUrl, options = {}) {
|
|
2468
|
-
return (await resolveMediaWithinBudget(pathOrUrl, options, MAX_INLINE_BYTES)).content;
|
|
2469
|
-
}
|
|
2470
|
-
var ViewFileInputSchema = z7.object({
|
|
2471
|
-
paths: z7.union([z7.string(), z7.array(z7.string()).max(MAX_VIEW_FILES)]).describe("Media URL(s) or file path(s) to view")
|
|
2472
|
-
});
|
|
2473
|
-
function mountViewFile(server, options = {}) {
|
|
2474
|
-
server.registerTool("view_file", {
|
|
2475
|
-
description: "View media (image, audio, video) by URL or local path — returns it as content you can SEE / HEAR. Pass several paths to view multiple files at once. Use it on a generation `output` url to inspect the result.",
|
|
2476
|
-
inputSchema: ViewFileInputSchema.shape,
|
|
2477
|
-
annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
|
|
2478
|
-
}, async (args) => {
|
|
2479
|
-
const list = Array.isArray(args.paths) ? args.paths : [args.paths];
|
|
2480
|
-
const content = [];
|
|
2481
|
-
let remainingBytes = MAX_INLINE_BYTES;
|
|
2482
|
-
for (const pathOrUrl of list) {
|
|
2483
|
-
try {
|
|
2484
|
-
const resolved = await resolveMediaWithinBudget(pathOrUrl, options, remainingBytes);
|
|
2485
|
-
remainingBytes -= resolved.bytes;
|
|
2486
|
-
content.push(...resolved.content);
|
|
2487
|
-
} catch (err) {
|
|
2488
|
-
content.push({
|
|
2489
|
-
type: "text",
|
|
2490
|
-
text: `[${pathOrUrl}] Error: ${err instanceof Error ? err.message : String(err)}`
|
|
2491
|
-
});
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
return { content };
|
|
2495
|
-
});
|
|
2496
|
-
}
|
|
2497
2813
|
export {
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
protectedResourceMetadataUrl,
|
|
2503
|
-
oauthProtectedResourceRoute,
|
|
2504
|
-
mountWait,
|
|
2505
|
-
mountViewFile,
|
|
2506
|
-
mountUpload,
|
|
2507
|
-
mountOAuthProvider,
|
|
2508
|
-
mountMcpResource,
|
|
2509
|
-
mountMcp,
|
|
2510
|
-
mountDownload,
|
|
2511
|
-
mountAgent,
|
|
2512
|
-
listToolNames,
|
|
2513
|
-
listContractToolNames,
|
|
2514
|
-
inlineMcpAppBundle,
|
|
2515
|
-
implementRemote,
|
|
2516
|
-
flattenToolJsonSchema,
|
|
2517
|
-
findUntypedProperties,
|
|
2518
|
-
findNonPortableFormats,
|
|
2519
|
-
defineRuntimeTool,
|
|
2520
|
-
createToolkit,
|
|
2521
|
-
createToolLogger,
|
|
2522
|
-
createToolInvoker,
|
|
2523
|
-
createStdioMcpServer,
|
|
2524
|
-
createSecureClientMetadataFetcher,
|
|
2525
|
-
createRuntimeToolFactory,
|
|
2526
|
-
createMcpHttpRoute,
|
|
2527
|
-
createMcpHandler,
|
|
2528
|
-
createCli,
|
|
2529
|
-
collectTools,
|
|
2530
|
-
coerceJsonArgs,
|
|
2531
|
-
buildToolManifest,
|
|
2532
|
-
buildMcpServer,
|
|
2533
|
-
RESOURCE_MIME_TYPE,
|
|
2534
|
-
PROTECTED_RESOURCE_PATH,
|
|
2814
|
+
AsyncOperationCancelResultSchema,
|
|
2815
|
+
EXT_APPS_BUNDLE_PLACEHOLDER,
|
|
2816
|
+
McpAnnotationsSchema,
|
|
2817
|
+
McpMediaContentSchema,
|
|
2535
2818
|
PORTABLE_JSON_SCHEMA_FORMATS,
|
|
2536
|
-
|
|
2819
|
+
PROTECTED_RESOURCE_PATH,
|
|
2820
|
+
RESOURCE_MIME_TYPE,
|
|
2821
|
+
UploadToolInputSchema,
|
|
2822
|
+
ViewFileErrorSchema,
|
|
2823
|
+
ViewFileInputSchema,
|
|
2824
|
+
ViewFileOutputSchema,
|
|
2825
|
+
bindContractAsyncOperation,
|
|
2826
|
+
bindStdioProcessSignals,
|
|
2827
|
+
buildMcpServer,
|
|
2828
|
+
buildToolManifest,
|
|
2829
|
+
coerceJsonArgs,
|
|
2830
|
+
collectTools,
|
|
2831
|
+
composeToolLifecycle,
|
|
2832
|
+
createAsyncOperationSnapshotSchema,
|
|
2833
|
+
createCli,
|
|
2834
|
+
createMcpHandler,
|
|
2835
|
+
createMcpHttpRoute,
|
|
2836
|
+
createRuntimeToolFactory,
|
|
2837
|
+
createSecureClientMetadataFetcher,
|
|
2838
|
+
createStdioMcpServer,
|
|
2839
|
+
createToolInvoker,
|
|
2840
|
+
createToolLogger,
|
|
2841
|
+
createToolkit,
|
|
2842
|
+
defineAsyncOperation,
|
|
2843
|
+
defineCliCommand,
|
|
2844
|
+
defineDownloadTool,
|
|
2845
|
+
defineRuntimeTool,
|
|
2846
|
+
defineUploadTool,
|
|
2847
|
+
defineViewFileTool,
|
|
2848
|
+
defineWaitTool,
|
|
2849
|
+
findNonPortableFormats,
|
|
2850
|
+
findUntypedProperties,
|
|
2851
|
+
flattenToolJsonSchema,
|
|
2852
|
+
inlineMcpAppBundle,
|
|
2853
|
+
listContractToolNames,
|
|
2854
|
+
listToolNames,
|
|
2855
|
+
mountAgent,
|
|
2856
|
+
mountDownload,
|
|
2857
|
+
mountMcp,
|
|
2858
|
+
mountMcpResource,
|
|
2859
|
+
mountOAuthProvider,
|
|
2860
|
+
mountUpload,
|
|
2861
|
+
mountViewFile,
|
|
2862
|
+
mountWait,
|
|
2863
|
+
oauthProtectedResourceRoute,
|
|
2864
|
+
protectedResourceMetadataUrl,
|
|
2865
|
+
resolveMedia,
|
|
2866
|
+
summarizeTransports,
|
|
2867
|
+
validateMcpSchemas,
|
|
2868
|
+
wwwAuthenticateHeader
|
|
2537
2869
|
};
|