toolcraft 0.0.104 → 0.0.106
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/composition.json +7 -2
- package/dist/cli.js +53 -0
- package/dist/composition.json +7 -2
- package/dist/index.d.ts +25 -3
- package/dist/index.js +18 -0
- package/dist/mcp.d.ts +6 -0
- package/dist/mcp.js +121 -1
- package/dist/sdk.d.ts +2 -1
- package/dist/sdk.js +56 -0
- package/dist/stream.d.ts +19 -0
- package/dist/stream.js +92 -0
- package/dist/testing/harness.d.ts +11 -0
- package/dist/testing/harness.js +70 -0
- package/dist/testing/index.d.ts +1 -1
- package/node_modules/tiny-stdio-mcp-server/LICENSE +21 -0
- package/node_modules/tiny-stdio-mcp-server/README.md +231 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/audio.d.ts +15 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/audio.js +84 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/convert.d.ts +16 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/convert.js +61 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/file-type.d.ts +11 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/file-type.js +93 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/file.d.ts +28 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/file.js +110 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/image.d.ts +15 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/image.js +72 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/index.d.ts +7 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/index.js +9 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/mime.d.ts +7 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/mime.js +52 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/remote.d.ts +5 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/remote.js +69 -0
- package/node_modules/tiny-stdio-mcp-server/dist/index.d.ts +9 -0
- package/node_modules/tiny-stdio-mcp-server/dist/index.js +7 -0
- package/node_modules/tiny-stdio-mcp-server/dist/jsonrpc.d.ts +14 -0
- package/node_modules/tiny-stdio-mcp-server/dist/jsonrpc.js +118 -0
- package/node_modules/tiny-stdio-mcp-server/dist/schema.d.ts +20 -0
- package/node_modules/tiny-stdio-mcp-server/dist/schema.js +26 -0
- package/node_modules/tiny-stdio-mcp-server/dist/server.d.ts +35 -0
- package/node_modules/tiny-stdio-mcp-server/dist/server.js +865 -0
- package/node_modules/tiny-stdio-mcp-server/dist/testing.d.ts +7 -0
- package/node_modules/tiny-stdio-mcp-server/dist/testing.js +20 -0
- package/node_modules/tiny-stdio-mcp-server/dist/types.d.ts +247 -0
- package/node_modules/tiny-stdio-mcp-server/dist/types.js +22 -0
- package/node_modules/tiny-stdio-mcp-server/package.json +56 -0
- package/node_modules/toolcraft-design/dist/explorer/keymap.js +2 -2
- package/node_modules/toolcraft-design/dist/explorer/render/modal.js +2 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +9 -5
package/dist/testing/harness.js
CHANGED
|
@@ -11,6 +11,7 @@ import { fakeFetch } from "./fakes.js";
|
|
|
11
11
|
import { createMemoryFs } from "./memory-fs.js";
|
|
12
12
|
import { runParity } from "./parity.js";
|
|
13
13
|
import { createRenderCapture } from "./render-capture.js";
|
|
14
|
+
import { createManagedStream } from "../stream.js";
|
|
14
15
|
function isHandlerFs(value) {
|
|
15
16
|
return typeof value.readFile === "function";
|
|
16
17
|
}
|
|
@@ -185,6 +186,75 @@ export function createCommandTestHarness(root, options = {}) {
|
|
|
185
186
|
return {
|
|
186
187
|
fs: memoryFs,
|
|
187
188
|
timeline: cumulativeTimeline,
|
|
189
|
+
async stream(requestedPath, inputParams = {}, streamOptions = {}) {
|
|
190
|
+
const events = [];
|
|
191
|
+
const statuses = [];
|
|
192
|
+
try {
|
|
193
|
+
const resolved = resolveCommand(root, requestedPath);
|
|
194
|
+
const command = resolved.command;
|
|
195
|
+
if (command.stream === undefined) {
|
|
196
|
+
throw new UserError(`Command "${resolved.path.join(".")}" is not a stream.`);
|
|
197
|
+
}
|
|
198
|
+
const sealedEnv = createSealedEnv(command.secrets, options.env, options.secrets);
|
|
199
|
+
const commandSecrets = resolveCommandSecrets(command, sealedEnv);
|
|
200
|
+
const diagnostics = createRuntimeLogger({
|
|
201
|
+
level: options.logLevel ?? "debug",
|
|
202
|
+
logger: () => undefined
|
|
203
|
+
});
|
|
204
|
+
const baseContext = {
|
|
205
|
+
...(options.services ?? {}),
|
|
206
|
+
secrets: commandSecrets,
|
|
207
|
+
fetch: runtimeFetch,
|
|
208
|
+
fs: memoryFs,
|
|
209
|
+
env: createEnv(sealedEnv),
|
|
210
|
+
diagnostics,
|
|
211
|
+
progress() { }
|
|
212
|
+
};
|
|
213
|
+
await assertCommandRequirements(command, { ...baseContext, params: undefined }, {
|
|
214
|
+
apiVersion: options.apiVersion,
|
|
215
|
+
env: sealedEnv
|
|
216
|
+
});
|
|
217
|
+
const schema = filterSchemaForScope(command.params, "sdk");
|
|
218
|
+
if (schema === undefined || schema.kind !== "object") {
|
|
219
|
+
throw new ToolcraftBugError(`command "${command.name}" must define an object params schema for SDK.`);
|
|
220
|
+
}
|
|
221
|
+
const errors = [];
|
|
222
|
+
const params = validateObjectSchema(schema, inputParams, "", errors);
|
|
223
|
+
throwValidationErrors(errors);
|
|
224
|
+
const stream = createManagedStream({
|
|
225
|
+
eventSchema: command.stream.event,
|
|
226
|
+
signal: streamOptions.signal,
|
|
227
|
+
onStatus: (event) => statuses.push(event),
|
|
228
|
+
async create(signal, status) {
|
|
229
|
+
return await command.handler({
|
|
230
|
+
...baseContext,
|
|
231
|
+
params,
|
|
232
|
+
signal,
|
|
233
|
+
status,
|
|
234
|
+
async refreshSecrets() {
|
|
235
|
+
return resolveCommandSecrets(command, sealedEnv);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
const limit = streamOptions.limit ?? Number.POSITIVE_INFINITY;
|
|
241
|
+
try {
|
|
242
|
+
for await (const event of stream) {
|
|
243
|
+
events.push(event);
|
|
244
|
+
if (events.length >= limit) {
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await stream.cancel();
|
|
251
|
+
}
|
|
252
|
+
return { ok: true, events, statuses };
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
return { ok: false, events, statuses, error };
|
|
256
|
+
}
|
|
257
|
+
},
|
|
188
258
|
async parity(requestedPath, inputParams = {}) {
|
|
189
259
|
const resolved = resolveCommand(root, requestedPath);
|
|
190
260
|
const sealedEnv = createSealedEnv(resolved.command.secrets, options.env, options.secrets);
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createCommandTestHarness, type CommandTestHarness, type ConfirmationRequest, type EffectEvent, type HarnessOptions, type PipelineStage, type RunResult } from "./harness.js";
|
|
1
|
+
export { createCommandTestHarness, type CommandTestHarness, type ConfirmationRequest, type EffectEvent, type HarnessOptions, type PipelineStage, type RunResult, type StreamRunResult } from "./harness.js";
|
|
2
2
|
export { fakeFetch, fakeService, type FetchRoute, type ServiceCall } from "./fakes.js";
|
|
3
3
|
export { createMemoryFs, type FsChange, type MemoryFs } from "./memory-fs.js";
|
|
4
4
|
export type { ParityResult, SurfaceOutcome } from "./parity.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Poe Platform
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# tiny-stdio-mcp-server
|
|
2
|
+
|
|
3
|
+
Minimal [Model Context Protocol](https://modelcontextprotocol.io) server for Node.js. Zero runtime dependencies, type-safe tool definitions, rich content helpers for images/audio/files.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install tiny-stdio-mcp-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createServer, defineSchema } from "tiny-stdio-mcp-server";
|
|
15
|
+
|
|
16
|
+
const schema = defineSchema({
|
|
17
|
+
text: { type: "string", description: "Text to reverse" }
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
createServer({ name: "my-server", version: "1.0.0" })
|
|
21
|
+
.tool("reverse", "Reverse a string", schema, ({ text }) => {
|
|
22
|
+
return text.split("").reverse().join("");
|
|
23
|
+
})
|
|
24
|
+
.listen();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run it:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
node my-server.js
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Any MCP client (Claude Code, Codex, etc.) can connect to it over stdio.
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### `createServer(options)`
|
|
38
|
+
|
|
39
|
+
Creates a new MCP server.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const server = createServer({ name: "my-server", version: "1.0.0" });
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### `.tool(name, description, schema, handler, outputSchema?)`
|
|
46
|
+
|
|
47
|
+
Register a tool. The handler receives typed args matching the schema and returns a string, content helper, array of content, or a typed object when `outputSchema` is supplied.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const schema = defineSchema({
|
|
51
|
+
query: { type: "string", description: "Search query" },
|
|
52
|
+
limit: { type: "number", description: "Max results", optional: true }
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
server.tool("search", "Search for things", schema, async ({ query, limit }) => {
|
|
56
|
+
// `query` is string, `limit` is number | undefined
|
|
57
|
+
return `Found results for: ${query}`;
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For structured-data tools, pass a root-object output schema. The server advertises it as MCP `Tool.outputSchema`, validates successful handler results, and returns them as `CallToolResult.structuredContent`. Plain object returns receive a JSON text backstop in `content[]` for older clients. Explicit `CallToolResult` values keep their handler-supplied content blocks; the JSON text backstop is added only when `content` is empty.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const input = defineSchema({
|
|
65
|
+
query: { type: "string" }
|
|
66
|
+
});
|
|
67
|
+
const output = defineSchema({
|
|
68
|
+
items: {
|
|
69
|
+
type: "array",
|
|
70
|
+
items: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
title: { type: "string" },
|
|
74
|
+
score: { type: "number" }
|
|
75
|
+
},
|
|
76
|
+
required: ["title", "score"]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
server.tool(
|
|
82
|
+
"search",
|
|
83
|
+
"Search",
|
|
84
|
+
input,
|
|
85
|
+
async ({ query }) => ({
|
|
86
|
+
items: [{ title: query, score: 1 }]
|
|
87
|
+
}),
|
|
88
|
+
output
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Output schemas must have `type: "object"` at the root because MCP structured content is an object. Otherwise, any schema accepted by Ajv is supported, including composition keywords, nullable type unions, and local `$defs`/`$ref` references. Input and output schemas compile synchronously during `.tool()` or `.registerTool()` registration, so malformed schemas throw immediately instead of failing on the first tool call.
|
|
93
|
+
|
|
94
|
+
Successful structured results must satisfy `outputSchema`. Validation failures use JSON-RPC `-32602` for inputs and `-32603` for outputs, with Ajv's formatted error text in the message and the raw Ajv error array in `error.data`. Explicit handler results with `isError: true` are passed through unchanged and are exempt from structured-content and output-schema validation.
|
|
95
|
+
|
|
96
|
+
Tools whose natural result is prose, images, audio, files, or other content blocks should omit `outputSchema` and keep returning content.
|
|
97
|
+
|
|
98
|
+
### `.listen()`
|
|
99
|
+
|
|
100
|
+
Start listening on stdin/stdout (standard MCP stdio transport).
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
await server.listen();
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `.connect(transport)`
|
|
107
|
+
|
|
108
|
+
Connect to a custom readable/writable stream pair.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
await server.connect({ readable: process.stdin, writable: process.stdout });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `.connectSDK(transport)`
|
|
115
|
+
|
|
116
|
+
Connect using an SDK-compatible in-memory transport (for testing).
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
await server.connectSDK(sdkTransport);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `.removeTool(name)` / `.notifyToolsChanged()`
|
|
123
|
+
|
|
124
|
+
Dynamically add/remove tools at runtime and notify connected clients.
|
|
125
|
+
|
|
126
|
+
## Configuration Options
|
|
127
|
+
|
|
128
|
+
- `createServer({ name, version, toolCallTimeoutMs? })`: server identity plus an optional positive-integer tool timeout in milliseconds. Timed-out calls return JSON-RPC error `-32603`; handlers continue running in the background.
|
|
129
|
+
- `.tool(name, description, schema, handler, outputSchema?)`: tool metadata, input schema, handler, and optional structured output schema.
|
|
130
|
+
- `.connect({ readable, writable })`: custom stream transport for tests or embedded hosts.
|
|
131
|
+
|
|
132
|
+
## `defineSchema(definition)`
|
|
133
|
+
|
|
134
|
+
Type-safe schema builder. Returns a JSON Schema object with inferred TypeScript types.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const schema = defineSchema({
|
|
138
|
+
name: { type: "string", description: "User name" },
|
|
139
|
+
age: { type: "number", description: "User age", optional: true }
|
|
140
|
+
});
|
|
141
|
+
// Handler receives: { name: string; age?: number }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Environment Variables
|
|
145
|
+
|
|
146
|
+
This package does not read public environment variables.
|
|
147
|
+
|
|
148
|
+
Supported types: `string`, `number`, `boolean`, `object`, `array`.
|
|
149
|
+
|
|
150
|
+
## Content helpers
|
|
151
|
+
|
|
152
|
+
Tool handlers can return rich content beyond plain text.
|
|
153
|
+
|
|
154
|
+
### Images
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { Image } from "tiny-stdio-mcp-server";
|
|
158
|
+
|
|
159
|
+
server.tool("screenshot", "Take a screenshot", schema, async () => {
|
|
160
|
+
return Image.fromBase64(base64Data, "image/png");
|
|
161
|
+
// or: await Image.fromUrl("https://example.com/image.png")
|
|
162
|
+
// or: Image.fromBytes(uint8Array)
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Supported formats: PNG, JPEG, GIF, WebP.
|
|
167
|
+
|
|
168
|
+
### Audio
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { Audio } from "tiny-stdio-mcp-server";
|
|
172
|
+
|
|
173
|
+
server.tool("speak", "Text to speech", schema, async () => {
|
|
174
|
+
return Audio.fromBase64(base64Data, "audio/mpeg");
|
|
175
|
+
// or: await Audio.fromUrl("https://example.com/audio.mp3")
|
|
176
|
+
// or: Audio.fromBytes(uint8Array, "mp3")
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Supported formats: MP3, WAV, OGG, M4A.
|
|
181
|
+
|
|
182
|
+
### Files
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { File } from "tiny-stdio-mcp-server";
|
|
186
|
+
|
|
187
|
+
server.tool("export", "Export data", schema, async () => {
|
|
188
|
+
return File.fromText(csvContent, "text/csv");
|
|
189
|
+
// or: File.fromBytes(uint8Array, "application/pdf")
|
|
190
|
+
// or: await File.fromUrl("https://example.com/report.pdf")
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Mixed content
|
|
195
|
+
|
|
196
|
+
Return arrays to send multiple content blocks:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
server.tool("analyze", "Analyze image", schema, async () => {
|
|
200
|
+
const image = await Image.fromUrl(url);
|
|
201
|
+
return [image, "Analysis complete: found 3 objects"];
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Testing
|
|
206
|
+
|
|
207
|
+
Use `createTestPair` with the official MCP SDK for in-memory testing:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { createTestPair } from "tiny-stdio-mcp-server/testing";
|
|
211
|
+
|
|
212
|
+
const server = createServer({ name: "test", version: "1.0.0" }).tool(
|
|
213
|
+
"ping",
|
|
214
|
+
"Ping",
|
|
215
|
+
defineSchema({}),
|
|
216
|
+
() => "pong"
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const { client, cleanup } = await createTestPair(server);
|
|
220
|
+
|
|
221
|
+
const result = await client.callTool({ name: "ping", arguments: {} });
|
|
222
|
+
// result.content === [{ type: "text", text: "pong" }]
|
|
223
|
+
|
|
224
|
+
await cleanup();
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Requires `@modelcontextprotocol/sdk` as a dev dependency.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type FromUrlOptions } from "./remote.js";
|
|
2
|
+
export interface AudioContent {
|
|
3
|
+
type: "audio";
|
|
4
|
+
data: string;
|
|
5
|
+
mimeType: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class Audio {
|
|
8
|
+
private readonly base64Data;
|
|
9
|
+
private readonly mimeType;
|
|
10
|
+
private constructor();
|
|
11
|
+
static fromUrl(url: string, options?: FromUrlOptions): Promise<Audio>;
|
|
12
|
+
static fromBytes(data: Uint8Array, format?: string): Audio;
|
|
13
|
+
static fromBase64(base64: string, mimeType: string): Audio;
|
|
14
|
+
toContentBlock(): AudioContent;
|
|
15
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { assertBase64, fileTypeFromBuffer, parseContentType, safeRemoteLabel } from "./mime.js";
|
|
2
|
+
import { readRemoteBytes } from "./remote.js";
|
|
3
|
+
const SUPPORTED_AUDIO_MIMES = new Set([
|
|
4
|
+
"audio/mpeg",
|
|
5
|
+
"audio/wav",
|
|
6
|
+
"audio/ogg",
|
|
7
|
+
"audio/mp4",
|
|
8
|
+
]);
|
|
9
|
+
const AUDIO_FORMAT_MAP = {
|
|
10
|
+
mp3: "audio/mpeg",
|
|
11
|
+
wav: "audio/wav",
|
|
12
|
+
ogg: "audio/ogg",
|
|
13
|
+
m4a: "audio/mp4",
|
|
14
|
+
mpeg: "audio/mpeg",
|
|
15
|
+
};
|
|
16
|
+
export class Audio {
|
|
17
|
+
base64Data;
|
|
18
|
+
mimeType;
|
|
19
|
+
constructor(base64Data, mimeType) {
|
|
20
|
+
this.base64Data = base64Data;
|
|
21
|
+
this.mimeType = mimeType;
|
|
22
|
+
}
|
|
23
|
+
static async fromUrl(url, options) {
|
|
24
|
+
const response = await fetch(url);
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Failed to fetch audio from ${safeRemoteLabel(url)}: ${response.status} ${response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
const data = await readRemoteBytes(response, "audio", url, options);
|
|
29
|
+
const detected = fileTypeFromBuffer(data);
|
|
30
|
+
let mimeType;
|
|
31
|
+
if (detected && SUPPORTED_AUDIO_MIMES.has(detected.mime)) {
|
|
32
|
+
mimeType = detected.mime;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const contentType = parseContentType(response.headers.get("content-type")).mimeType;
|
|
36
|
+
if (contentType && SUPPORTED_AUDIO_MIMES.has(contentType)) {
|
|
37
|
+
mimeType = contentType;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
throw new Error(`Unable to detect audio MIME type from ${safeRemoteLabel(url)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const base64 = Buffer.from(data).toString("base64");
|
|
44
|
+
return new Audio(base64, mimeType);
|
|
45
|
+
}
|
|
46
|
+
static fromBytes(data, format) {
|
|
47
|
+
let mimeType;
|
|
48
|
+
if (format) {
|
|
49
|
+
if (format.includes("/")) {
|
|
50
|
+
mimeType = format.toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
mimeType = AUDIO_FORMAT_MAP[format.toLowerCase()] || `audio/${format}`;
|
|
54
|
+
}
|
|
55
|
+
if (!SUPPORTED_AUDIO_MIMES.has(mimeType)) {
|
|
56
|
+
throw new Error(`Unsupported audio MIME type: ${mimeType}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const detected = fileTypeFromBuffer(data);
|
|
61
|
+
if (!detected || !SUPPORTED_AUDIO_MIMES.has(detected.mime)) {
|
|
62
|
+
throw new Error("Unable to detect audio MIME type from bytes");
|
|
63
|
+
}
|
|
64
|
+
mimeType = detected.mime;
|
|
65
|
+
}
|
|
66
|
+
const base64 = Buffer.from(data).toString("base64");
|
|
67
|
+
return new Audio(base64, mimeType);
|
|
68
|
+
}
|
|
69
|
+
static fromBase64(base64, mimeType) {
|
|
70
|
+
assertBase64(base64);
|
|
71
|
+
const normalizedMimeType = mimeType.toLowerCase();
|
|
72
|
+
if (!SUPPORTED_AUDIO_MIMES.has(normalizedMimeType)) {
|
|
73
|
+
throw new Error(`Unsupported audio MIME type: ${normalizedMimeType}`);
|
|
74
|
+
}
|
|
75
|
+
return new Audio(base64, normalizedMimeType);
|
|
76
|
+
}
|
|
77
|
+
toContentBlock() {
|
|
78
|
+
return {
|
|
79
|
+
type: "audio",
|
|
80
|
+
data: this.base64Data,
|
|
81
|
+
mimeType: this.mimeType,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Image, type ImageContent } from "./image.js";
|
|
2
|
+
import { Audio, type AudioContent } from "./audio.js";
|
|
3
|
+
import { File, type EmbeddedResource } from "./file.js";
|
|
4
|
+
export interface TextContent {
|
|
5
|
+
type: "text";
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
export type ContentBlock = TextContent | ImageContent | AudioContent | EmbeddedResource;
|
|
9
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
10
|
+
export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
|
|
11
|
+
export type JsonObject = {
|
|
12
|
+
[key: string]: JsonValue;
|
|
13
|
+
};
|
|
14
|
+
export type ToolReturn = undefined | JsonPrimitive | JsonObject | Image | Audio | File | ContentBlock | Array<undefined | JsonPrimitive | JsonObject | Image | Audio | File | ContentBlock>;
|
|
15
|
+
export declare function toContentBlocks(result: ToolReturn): ContentBlock[];
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Image } from "./image.js";
|
|
2
|
+
import { Audio } from "./audio.js";
|
|
3
|
+
import { File } from "./file.js";
|
|
4
|
+
function convertSingleValue(value) {
|
|
5
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
6
|
+
return { type: "text", text: String(value) };
|
|
7
|
+
}
|
|
8
|
+
if (value === null) {
|
|
9
|
+
return { type: "text", text: "null" };
|
|
10
|
+
}
|
|
11
|
+
if (value instanceof Image) {
|
|
12
|
+
return value.toContentBlock();
|
|
13
|
+
}
|
|
14
|
+
if (value instanceof Audio) {
|
|
15
|
+
return value.toContentBlock();
|
|
16
|
+
}
|
|
17
|
+
if (value instanceof File) {
|
|
18
|
+
return value.toContentBlock();
|
|
19
|
+
}
|
|
20
|
+
if (isContentBlock(value)) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
return { type: "text", text: JSON.stringify(value) };
|
|
24
|
+
}
|
|
25
|
+
export function toContentBlocks(result) {
|
|
26
|
+
if (result === undefined) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(result)) {
|
|
30
|
+
return result.flatMap((item) => toContentBlocks(item));
|
|
31
|
+
}
|
|
32
|
+
return [convertSingleValue(result)];
|
|
33
|
+
}
|
|
34
|
+
function isContentBlock(value) {
|
|
35
|
+
if (!hasOwnProperty(value, "type") || typeof value.type !== "string") {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (value.type === "text") {
|
|
39
|
+
return hasOwnProperty(value, "text") && typeof value.text === "string";
|
|
40
|
+
}
|
|
41
|
+
if (value.type === "image" || value.type === "audio") {
|
|
42
|
+
return hasOwnProperty(value, "data")
|
|
43
|
+
&& typeof value.data === "string"
|
|
44
|
+
&& hasOwnProperty(value, "mimeType")
|
|
45
|
+
&& typeof value.mimeType === "string";
|
|
46
|
+
}
|
|
47
|
+
if (value.type !== "resource" || !hasOwnProperty(value, "resource")) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const resource = value.resource;
|
|
51
|
+
return typeof resource === "object"
|
|
52
|
+
&& resource !== null
|
|
53
|
+
&& hasOwnProperty(resource, "uri")
|
|
54
|
+
&& typeof resource.uri === "string"
|
|
55
|
+
&& (!hasOwnProperty(resource, "mimeType") || typeof resource.mimeType === "string")
|
|
56
|
+
&& ((hasOwnProperty(resource, "text") && typeof resource.text === "string")
|
|
57
|
+
|| (hasOwnProperty(resource, "blob") && typeof resource.blob === "string"));
|
|
58
|
+
}
|
|
59
|
+
function hasOwnProperty(value, name) {
|
|
60
|
+
return Object.prototype.hasOwnProperty.call(value, name);
|
|
61
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal magic bytes detection for common media types.
|
|
3
|
+
* This can be replaced with `file-type` package (https://npm.im/file-type)
|
|
4
|
+
* if more comprehensive detection is needed. The API is designed to be
|
|
5
|
+
* compatible: fileTypeFromBuffer(data) returns { mime: string, ext: string } | undefined
|
|
6
|
+
*/
|
|
7
|
+
export interface FileTypeResult {
|
|
8
|
+
mime: string;
|
|
9
|
+
ext: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function fileTypeFromBuffer(data: Uint8Array): FileTypeResult | undefined;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal magic bytes detection for common media types.
|
|
3
|
+
* This can be replaced with `file-type` package (https://npm.im/file-type)
|
|
4
|
+
* if more comprehensive detection is needed. The API is designed to be
|
|
5
|
+
* compatible: fileTypeFromBuffer(data) returns { mime: string, ext: string } | undefined
|
|
6
|
+
*/
|
|
7
|
+
export function fileTypeFromBuffer(data) {
|
|
8
|
+
if (data.length < 12) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
12
|
+
if (data[0] === 0x89 &&
|
|
13
|
+
data[1] === 0x50 &&
|
|
14
|
+
data[2] === 0x4e &&
|
|
15
|
+
data[3] === 0x47 &&
|
|
16
|
+
data[4] === 0x0d &&
|
|
17
|
+
data[5] === 0x0a &&
|
|
18
|
+
data[6] === 0x1a &&
|
|
19
|
+
data[7] === 0x0a) {
|
|
20
|
+
return { mime: "image/png", ext: "png" };
|
|
21
|
+
}
|
|
22
|
+
// JPEG: FF D8 FF
|
|
23
|
+
if (data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) {
|
|
24
|
+
return { mime: "image/jpeg", ext: "jpg" };
|
|
25
|
+
}
|
|
26
|
+
// GIF: 47 49 46 38 (GIF8)
|
|
27
|
+
if (data[0] === 0x47 &&
|
|
28
|
+
data[1] === 0x49 &&
|
|
29
|
+
data[2] === 0x46 &&
|
|
30
|
+
data[3] === 0x38) {
|
|
31
|
+
return { mime: "image/gif", ext: "gif" };
|
|
32
|
+
}
|
|
33
|
+
// WEBP: 52 49 46 46 ... 57 45 42 50 (RIFF...WEBP)
|
|
34
|
+
if (data[0] === 0x52 &&
|
|
35
|
+
data[1] === 0x49 &&
|
|
36
|
+
data[2] === 0x46 &&
|
|
37
|
+
data[3] === 0x46 &&
|
|
38
|
+
data[8] === 0x57 &&
|
|
39
|
+
data[9] === 0x45 &&
|
|
40
|
+
data[10] === 0x42 &&
|
|
41
|
+
data[11] === 0x50) {
|
|
42
|
+
return { mime: "image/webp", ext: "webp" };
|
|
43
|
+
}
|
|
44
|
+
// MP3: FF FB or FF FA (MPEG audio) or 49 44 33 (ID3 tag)
|
|
45
|
+
if ((data[0] === 0xff && (data[1] === 0xfb || data[1] === 0xfa)) ||
|
|
46
|
+
(data[0] === 0x49 && data[1] === 0x44 && data[2] === 0x33)) {
|
|
47
|
+
return { mime: "audio/mpeg", ext: "mp3" };
|
|
48
|
+
}
|
|
49
|
+
// WAV: 52 49 46 46 ... 57 41 56 45 (RIFF...WAVE)
|
|
50
|
+
if (data[0] === 0x52 &&
|
|
51
|
+
data[1] === 0x49 &&
|
|
52
|
+
data[2] === 0x46 &&
|
|
53
|
+
data[3] === 0x46 &&
|
|
54
|
+
data[8] === 0x57 &&
|
|
55
|
+
data[9] === 0x41 &&
|
|
56
|
+
data[10] === 0x56 &&
|
|
57
|
+
data[11] === 0x45) {
|
|
58
|
+
return { mime: "audio/wav", ext: "wav" };
|
|
59
|
+
}
|
|
60
|
+
// OGG: 4F 67 67 53 (OggS)
|
|
61
|
+
if (data[0] === 0x4f &&
|
|
62
|
+
data[1] === 0x67 &&
|
|
63
|
+
data[2] === 0x67 &&
|
|
64
|
+
data[3] === 0x53) {
|
|
65
|
+
return { mime: "audio/ogg", ext: "ogg" };
|
|
66
|
+
}
|
|
67
|
+
// M4A: MP4 container with audio - check for M4A brand at offset 8
|
|
68
|
+
// ftyp followed by M4A brand
|
|
69
|
+
if (data[4] === 0x66 &&
|
|
70
|
+
data[5] === 0x74 &&
|
|
71
|
+
data[6] === 0x79 &&
|
|
72
|
+
data[7] === 0x70 &&
|
|
73
|
+
data[8] === 0x4d &&
|
|
74
|
+
data[9] === 0x34 &&
|
|
75
|
+
data[10] === 0x41) {
|
|
76
|
+
return { mime: "audio/mp4", ext: "m4a" };
|
|
77
|
+
}
|
|
78
|
+
// MP4: ... 66 74 79 70 (ftyp box, offset 4)
|
|
79
|
+
if (data[4] === 0x66 &&
|
|
80
|
+
data[5] === 0x74 &&
|
|
81
|
+
data[6] === 0x79 &&
|
|
82
|
+
data[7] === 0x70) {
|
|
83
|
+
return { mime: "video/mp4", ext: "mp4" };
|
|
84
|
+
}
|
|
85
|
+
// WEBM: 1A 45 DF A3 (EBML header)
|
|
86
|
+
if (data[0] === 0x1a &&
|
|
87
|
+
data[1] === 0x45 &&
|
|
88
|
+
data[2] === 0xdf &&
|
|
89
|
+
data[3] === 0xa3) {
|
|
90
|
+
return { mime: "video/webm", ext: "webm" };
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type FromUrlOptions } from "./remote.js";
|
|
2
|
+
export interface TextResourceContents {
|
|
3
|
+
uri: string;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BlobResourceContents {
|
|
8
|
+
uri: string;
|
|
9
|
+
mimeType: string;
|
|
10
|
+
blob: string;
|
|
11
|
+
}
|
|
12
|
+
export interface EmbeddedResource {
|
|
13
|
+
type: "resource";
|
|
14
|
+
resource: TextResourceContents | BlobResourceContents;
|
|
15
|
+
}
|
|
16
|
+
export declare class File {
|
|
17
|
+
private readonly data;
|
|
18
|
+
private readonly mimeType;
|
|
19
|
+
private readonly isText;
|
|
20
|
+
private readonly name?;
|
|
21
|
+
private readonly charset;
|
|
22
|
+
private constructor();
|
|
23
|
+
static fromUrl(url: string, options?: FromUrlOptions): Promise<File>;
|
|
24
|
+
static fromBytes(data: Uint8Array, mimeType: string): File;
|
|
25
|
+
static fromText(text: string, mimeType?: string): File;
|
|
26
|
+
static fromBase64(base64: string, mimeType: string): File;
|
|
27
|
+
toContentBlock(): EmbeddedResource;
|
|
28
|
+
}
|