toolcraft 0.0.103 → 0.0.105
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-schema/package.json +1 -1
- package/package.json +9 -5
package/composition.json
CHANGED
|
@@ -46,9 +46,14 @@
|
|
|
46
46
|
"version": "0.1.0",
|
|
47
47
|
"license": "MIT"
|
|
48
48
|
},
|
|
49
|
+
{
|
|
50
|
+
"name": "tiny-stdio-mcp-server",
|
|
51
|
+
"version": "0.1.0",
|
|
52
|
+
"license": "MIT"
|
|
53
|
+
},
|
|
49
54
|
{
|
|
50
55
|
"name": "toolcraft",
|
|
51
|
-
"version": "0.0.
|
|
56
|
+
"version": "0.0.105",
|
|
52
57
|
"license": "MIT"
|
|
53
58
|
},
|
|
54
59
|
{
|
|
@@ -58,7 +63,7 @@
|
|
|
58
63
|
},
|
|
59
64
|
{
|
|
60
65
|
"name": "toolcraft-schema",
|
|
61
|
-
"version": "0.0.
|
|
66
|
+
"version": "0.0.105",
|
|
62
67
|
"license": "MIT"
|
|
63
68
|
}
|
|
64
69
|
]
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import { enableSourceMaps, formatDebugStack } from "./stack-trim.js";
|
|
|
18
18
|
import { suggest } from "./suggest.js";
|
|
19
19
|
import { throwValidationErrors } from "./validation-errors.js";
|
|
20
20
|
import { RESERVED_SERVICE_NAMES, createEnv, createFs, validateServices } from "./runtime/io.js";
|
|
21
|
+
import { createManagedStream } from "./stream.js";
|
|
21
22
|
export { renderErrorReport } from "./error-report.js";
|
|
22
23
|
configureTheme({ brand: "blue", label: "Toolcraft" });
|
|
23
24
|
export { configureTheme };
|
|
@@ -3050,6 +3051,58 @@ async function executeCommand(state, services, requirementOptions, runtimeFetch,
|
|
|
3050
3051
|
...preflightContext,
|
|
3051
3052
|
params
|
|
3052
3053
|
};
|
|
3054
|
+
if (state.command.stream !== undefined) {
|
|
3055
|
+
const stream = createManagedStream({
|
|
3056
|
+
eventSchema: state.command.stream.event,
|
|
3057
|
+
onStatus(event) {
|
|
3058
|
+
diagnostics.emit({
|
|
3059
|
+
level: "info",
|
|
3060
|
+
message: event.message ?? event.type,
|
|
3061
|
+
category: "runtime"
|
|
3062
|
+
});
|
|
3063
|
+
if (output === "rich" && event.message !== undefined) {
|
|
3064
|
+
logger.info(event.message);
|
|
3065
|
+
}
|
|
3066
|
+
},
|
|
3067
|
+
async create(signal, status) {
|
|
3068
|
+
return await state.command.handler({
|
|
3069
|
+
...context,
|
|
3070
|
+
signal,
|
|
3071
|
+
status,
|
|
3072
|
+
async refreshSecrets() {
|
|
3073
|
+
return resolveCommandSecrets(state.command, runtimeEnv);
|
|
3074
|
+
}
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
});
|
|
3078
|
+
const interrupt = () => {
|
|
3079
|
+
void stream.cancel(new UserError("Operation cancelled."));
|
|
3080
|
+
};
|
|
3081
|
+
process.once("SIGINT", interrupt);
|
|
3082
|
+
try {
|
|
3083
|
+
for await (const event of stream) {
|
|
3084
|
+
if (output === "json") {
|
|
3085
|
+
const line = JSON.stringify(event);
|
|
3086
|
+
if (outputEmitter === undefined) {
|
|
3087
|
+
process.stdout.write(`${line}\n`);
|
|
3088
|
+
}
|
|
3089
|
+
else {
|
|
3090
|
+
outputEmitter(line);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
else {
|
|
3094
|
+
renderResult(state.command, event, output, primitives, outputEmitter === undefined
|
|
3095
|
+
? undefined
|
|
3096
|
+
: (chunk) => outputEmitter(chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk));
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
finally {
|
|
3101
|
+
process.removeListener("SIGINT", interrupt);
|
|
3102
|
+
await stream.cancel();
|
|
3103
|
+
}
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3053
3106
|
if (state.command.confirm &&
|
|
3054
3107
|
!state.command.humanInLoop &&
|
|
3055
3108
|
!resolvedFlags.yes &&
|
package/dist/composition.json
CHANGED
|
@@ -46,9 +46,14 @@
|
|
|
46
46
|
"version": "0.1.0",
|
|
47
47
|
"license": "MIT"
|
|
48
48
|
},
|
|
49
|
+
{
|
|
50
|
+
"name": "tiny-stdio-mcp-server",
|
|
51
|
+
"version": "0.1.0",
|
|
52
|
+
"license": "MIT"
|
|
53
|
+
},
|
|
49
54
|
{
|
|
50
55
|
"name": "toolcraft",
|
|
51
|
-
"version": "0.0.
|
|
56
|
+
"version": "0.0.105",
|
|
52
57
|
"license": "MIT"
|
|
53
58
|
},
|
|
54
59
|
{
|
|
@@ -58,7 +63,7 @@
|
|
|
58
63
|
},
|
|
59
64
|
{
|
|
60
65
|
"name": "toolcraft-schema",
|
|
61
|
-
"version": "0.0.
|
|
66
|
+
"version": "0.0.105",
|
|
62
67
|
"license": "MIT"
|
|
63
68
|
}
|
|
64
69
|
]
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { McpServerConfig } from "@poe-code/agent-mcp-config";
|
|
2
|
-
import type { ObjectSchema, Static } from "toolcraft-schema";
|
|
2
|
+
import type { AnySchema, ObjectSchema, Static } from "toolcraft-schema";
|
|
3
3
|
import type { LoggerOutput, RenderTableOptions, ThemePalette } from "toolcraft-design";
|
|
4
4
|
import { ApprovalDeclinedError } from "./human-in-loop/types.js";
|
|
5
5
|
import type { HumanInLoopConfig, HumanInLoopPending, HumanInLoopRuntimeOptions } from "./human-in-loop/types.js";
|
|
6
6
|
import { ToolcraftBugError, UserError } from "./user-error.js";
|
|
7
7
|
import type { RuntimeLogger } from "./runtime-logging.js";
|
|
8
|
+
import type { StreamStatusEvent, ToolcraftStream } from "./stream.js";
|
|
9
|
+
export { createManagedStream } from "./stream.js";
|
|
10
|
+
export type { StreamConsumerOptions, StreamStatusEvent, StreamStatusType, ToolcraftStream } from "./stream.js";
|
|
8
11
|
type ScopeValue = "cli" | "mcp" | "sdk";
|
|
9
12
|
type AnyObjectSchema = ObjectSchema<Record<string, never>>;
|
|
10
13
|
type EmptyServices = Record<string, never>;
|
|
@@ -114,9 +117,23 @@ export interface CommandConfig<TServices extends object, TParamsSchema extends O
|
|
|
114
117
|
confirm?: boolean;
|
|
115
118
|
humanInLoop?: HumanInLoopConfig<TParamsSchema> | null;
|
|
116
119
|
requires?: Requires<CommandCheckContext<TParamsSchema, TSecrets, TServices>>;
|
|
117
|
-
handler: (ctx: HandlerContext<TParamsSchema, TSecrets, TServices>) => Promise<TResult
|
|
120
|
+
handler: (ctx: HandlerContext<TParamsSchema, TSecrets, TServices>) => Promise<TResult> | TResult;
|
|
118
121
|
render?: Renderers<TResult>;
|
|
119
122
|
}
|
|
123
|
+
export interface StreamDefinition<TEventSchema extends AnySchema = AnySchema> {
|
|
124
|
+
event: TEventSchema;
|
|
125
|
+
bufferSize: number;
|
|
126
|
+
}
|
|
127
|
+
export type StreamHandlerContext<TParamsSchema extends ObjectSchema<any>, TSecrets extends SecretDeclarations | undefined, TServices extends object> = HandlerContext<TParamsSchema, TSecrets, TServices> & {
|
|
128
|
+
signal: AbortSignal;
|
|
129
|
+
status(event: StreamStatusEvent): void;
|
|
130
|
+
refreshSecrets(): Promise<InferSecrets<TSecrets>>;
|
|
131
|
+
};
|
|
132
|
+
export interface StreamCommandConfig<TServices extends object, TParamsSchema extends ObjectSchema<any>, TSecrets extends SecretDeclarations | undefined, TEventSchema extends AnySchema> extends Omit<CommandConfig<TServices, TParamsSchema, TSecrets, AsyncIterable<Static<TEventSchema>>>, "handler" | "render" | "result" | "humanInLoop" | "confirm"> {
|
|
133
|
+
event: TEventSchema;
|
|
134
|
+
handler: (ctx: StreamHandlerContext<TParamsSchema, TSecrets, TServices>) => AsyncIterable<Static<TEventSchema>> | Promise<AsyncIterable<Static<TEventSchema>>>;
|
|
135
|
+
render?: Renderers<Static<TEventSchema>>;
|
|
136
|
+
}
|
|
120
137
|
export interface Command<TServices extends object = EmptyServices, TParamsSchema extends ObjectSchema<any> = AnyObjectSchema, TSecrets extends SecretDeclarations | undefined = undefined, TResult = unknown> {
|
|
121
138
|
kind: "command";
|
|
122
139
|
name: string;
|
|
@@ -127,12 +144,13 @@ export interface Command<TServices extends object = EmptyServices, TParamsSchema
|
|
|
127
144
|
positional: string[];
|
|
128
145
|
params: TParamsSchema;
|
|
129
146
|
result?: ObjectSchema<any>;
|
|
147
|
+
stream?: StreamDefinition<any>;
|
|
130
148
|
secrets: SecretDeclarations;
|
|
131
149
|
scope: Scope[];
|
|
132
150
|
confirm: boolean;
|
|
133
151
|
humanInLoop?: HumanInLoopConfig<TParamsSchema> | null;
|
|
134
152
|
requires?: Requires<any>;
|
|
135
|
-
handler: (ctx: HandlerContext<TParamsSchema, TSecrets, TServices>) => Promise<TResult
|
|
153
|
+
handler: (ctx: HandlerContext<TParamsSchema, TSecrets, TServices>) => Promise<TResult> | TResult;
|
|
136
154
|
render?: Renderers<TResult>;
|
|
137
155
|
}
|
|
138
156
|
export interface GroupConfig<TServices extends object> {
|
|
@@ -193,6 +211,10 @@ export declare function defineCommand<TServices extends object = EmptyServices,
|
|
|
193
211
|
scope?: TOwnScope;
|
|
194
212
|
humanInLoop?: TOwnHumanInLoop;
|
|
195
213
|
}): Command<TServices, TParamsSchema, TSecrets, TResult> & TypedCommandMetadata<TName, TParamsSchema, TResult, TOwnScope, ResolveOwnHumanInLoopMode<TOwnHumanInLoop>>;
|
|
214
|
+
export declare function defineStreamCommand<TServices extends object = EmptyServices, TName extends string = string, TParamsSchema extends ObjectSchema<any> = AnyObjectSchema, TSecrets extends SecretDeclarations | undefined = undefined, TEventSchema extends AnySchema = AnySchema, TOwnScope extends ScopeInput = undefined>(config: Omit<StreamCommandConfig<TServices, TParamsSchema, TSecrets, TEventSchema>, "name" | "scope"> & {
|
|
215
|
+
name: TName;
|
|
216
|
+
scope?: TOwnScope;
|
|
217
|
+
}): Command<TServices, TParamsSchema, TSecrets, AsyncIterable<Static<TEventSchema>>> & TypedCommandMetadata<TName, TParamsSchema, ToolcraftStream<Static<TEventSchema>>, TOwnScope, undefined>;
|
|
196
218
|
export declare function defineGroup<TServices extends object = EmptyServices, TName extends string = string, TChildren extends readonly unknown[] = readonly CommandNode<TServices>[], TOwnScope extends ScopeInput = undefined, TOwnHumanInLoop extends HumanInLoopConfig<AnyObjectSchema> | null | undefined = undefined>(config: Omit<GroupConfig<TServices>, "name" | "children" | "scope" | "humanInLoop"> & {
|
|
197
219
|
name: TName;
|
|
198
220
|
children: TChildren & readonly CommandNode<TServices>[];
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ApprovalDeclinedError } from "./human-in-loop/types.js";
|
|
|
3
3
|
import { mergeHumanInLoopFromGroup, validateHumanInLoopOnDefine } from "./human-in-loop/config.js";
|
|
4
4
|
import { ToolcraftBugError, UserError } from "./user-error.js";
|
|
5
5
|
import { suggest } from "./suggest.js";
|
|
6
|
+
export { createManagedStream } from "./stream.js";
|
|
6
7
|
const commandConfigSymbol = Symbol("toolcraft.command.config");
|
|
7
8
|
const groupConfigSymbol = Symbol("toolcraft.group.config");
|
|
8
9
|
const commandSourcePathSymbol = Symbol("toolcraft.command.sourcePath");
|
|
@@ -297,6 +298,7 @@ function createBaseCommand(config) {
|
|
|
297
298
|
positional: [...(config.positional ?? [])],
|
|
298
299
|
params: config.params,
|
|
299
300
|
result: config.result,
|
|
301
|
+
stream: undefined,
|
|
300
302
|
secrets: cloneSecrets(config.secrets),
|
|
301
303
|
scope: resolveCommandScope(config.scope, undefined),
|
|
302
304
|
confirm: config.confirm ?? false,
|
|
@@ -365,6 +367,7 @@ function materializeCommand(command, inherited) {
|
|
|
365
367
|
positional: [...command.positional],
|
|
366
368
|
params: command.params,
|
|
367
369
|
result: internal.result,
|
|
370
|
+
stream: command.stream,
|
|
368
371
|
secrets: mergeSecrets(inherited.secrets, internal.secrets),
|
|
369
372
|
scope: resolveCommandScope(internal.scope, inherited.scope),
|
|
370
373
|
confirm: command.confirm,
|
|
@@ -456,6 +459,21 @@ export function defineCommand(config) {
|
|
|
456
459
|
requires: undefined
|
|
457
460
|
});
|
|
458
461
|
}
|
|
462
|
+
export function defineStreamCommand(config) {
|
|
463
|
+
const command = defineCommand({
|
|
464
|
+
...config,
|
|
465
|
+
confirm: false,
|
|
466
|
+
humanInLoop: null,
|
|
467
|
+
handler: config.handler,
|
|
468
|
+
render: undefined
|
|
469
|
+
});
|
|
470
|
+
command.stream = {
|
|
471
|
+
event: config.event,
|
|
472
|
+
bufferSize: 1
|
|
473
|
+
};
|
|
474
|
+
command.render = config.render;
|
|
475
|
+
return command;
|
|
476
|
+
}
|
|
459
477
|
export function defineGroup(config) {
|
|
460
478
|
validateRenameMap(config.rename);
|
|
461
479
|
validateHumanInLoopOnDefine(config);
|
package/dist/mcp.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ type Casing = "snake" | "camel";
|
|
|
6
6
|
type CmdkitServer = Omit<TinyServer, "connect"> & {
|
|
7
7
|
connect(transport: SDKTransport): Promise<void>;
|
|
8
8
|
};
|
|
9
|
+
export declare const MCP_STREAM_METHODS: {
|
|
10
|
+
readonly list: "toolcraft/streams/list";
|
|
11
|
+
readonly subscribe: "toolcraft/streams/subscribe";
|
|
12
|
+
readonly unsubscribe: "toolcraft/streams/unsubscribe";
|
|
13
|
+
readonly notification: "notifications/toolcraft/stream";
|
|
14
|
+
};
|
|
9
15
|
export interface RunMCPOptions<TServices extends object = Record<string, unknown>> {
|
|
10
16
|
apiVersion?: string;
|
|
11
17
|
approvals?: boolean;
|
package/dist/mcp.js
CHANGED
|
@@ -14,6 +14,13 @@ import { suggest } from "./suggest.js";
|
|
|
14
14
|
import { throwValidationErrors } from "./validation-errors.js";
|
|
15
15
|
import { createRuntimeLogger } from "./runtime-logging.js";
|
|
16
16
|
import { createEnv, createFs, validateServices } from "./runtime/io.js";
|
|
17
|
+
import { createManagedStream } from "./stream.js";
|
|
18
|
+
export const MCP_STREAM_METHODS = {
|
|
19
|
+
list: "toolcraft/streams/list",
|
|
20
|
+
subscribe: "toolcraft/streams/subscribe",
|
|
21
|
+
unsubscribe: "toolcraft/streams/unsubscribe",
|
|
22
|
+
notification: "notifications/toolcraft/stream"
|
|
23
|
+
};
|
|
17
24
|
function normalizeRoots(roots) {
|
|
18
25
|
if (!Array.isArray(roots)) {
|
|
19
26
|
return roots;
|
|
@@ -728,7 +735,120 @@ function createResolvedMCPServer(root, options) {
|
|
|
728
735
|
version,
|
|
729
736
|
validateToolArguments: false
|
|
730
737
|
});
|
|
731
|
-
|
|
738
|
+
const streamTools = tools.filter((tool) => tool.command.stream !== undefined);
|
|
739
|
+
const subscriptions = new Map();
|
|
740
|
+
let nextSubscriptionId = 0;
|
|
741
|
+
server.method(MCP_STREAM_METHODS.list, () => ({
|
|
742
|
+
streams: streamTools.map((tool) => ({
|
|
743
|
+
name: tool.name,
|
|
744
|
+
description: tool.description,
|
|
745
|
+
inputSchema: tool.inputSchema,
|
|
746
|
+
eventSchema: applySchemaCasing(toJsonSchema(tool.command.stream.event), casing),
|
|
747
|
+
bufferSize: tool.command.stream.bufferSize
|
|
748
|
+
}))
|
|
749
|
+
}));
|
|
750
|
+
server.method(MCP_STREAM_METHODS.subscribe, async (request, session) => {
|
|
751
|
+
const name = typeof request?.name === "string" ? request.name : undefined;
|
|
752
|
+
const tool = streamTools.find((candidate) => candidate.name === name);
|
|
753
|
+
if (tool === undefined) {
|
|
754
|
+
throw new UserError(`Stream not found: ${name ?? ""}`);
|
|
755
|
+
}
|
|
756
|
+
const streamDefinition = tool.command.stream;
|
|
757
|
+
if (streamDefinition === undefined) {
|
|
758
|
+
throw new ToolcraftBugError(`Command "${tool.commandPath}" is not a stream.`);
|
|
759
|
+
}
|
|
760
|
+
const argumentsValue = isPlainObject(request?.arguments) ? request.arguments : {};
|
|
761
|
+
const secrets = resolveCommandSecrets(tool.command, options.env);
|
|
762
|
+
const baseContext = {
|
|
763
|
+
...servicesWithBuiltIns,
|
|
764
|
+
secrets,
|
|
765
|
+
fetch: runtimeFetch,
|
|
766
|
+
fs: createFs(options.fs),
|
|
767
|
+
env: createEnv(options.env),
|
|
768
|
+
diagnostics,
|
|
769
|
+
progress(message) {
|
|
770
|
+
diagnostics.emit({ level: "info", message, category: "progress" });
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
await assertCommandRequirements(tool.command, { ...baseContext, params: undefined }, { apiVersion: options.apiVersion, env: options.env });
|
|
774
|
+
const params = validateToolArguments(tool.command.params, argumentsValue, casing);
|
|
775
|
+
const subscriptionId = `stream-${++nextSubscriptionId}`;
|
|
776
|
+
let notificationQueue = Promise.resolve();
|
|
777
|
+
const notify = (params) => {
|
|
778
|
+
notificationQueue = notificationQueue.then(() => session.notify(MCP_STREAM_METHODS.notification, params));
|
|
779
|
+
return notificationQueue;
|
|
780
|
+
};
|
|
781
|
+
const stream = createManagedStream({
|
|
782
|
+
eventSchema: streamDefinition.event,
|
|
783
|
+
signal: session.signal,
|
|
784
|
+
onStatus(event) {
|
|
785
|
+
void notify({
|
|
786
|
+
subscriptionId,
|
|
787
|
+
type: "status",
|
|
788
|
+
status: event
|
|
789
|
+
});
|
|
790
|
+
},
|
|
791
|
+
async create(signal, status) {
|
|
792
|
+
return await tool.command.handler({
|
|
793
|
+
...baseContext,
|
|
794
|
+
params,
|
|
795
|
+
signal,
|
|
796
|
+
status,
|
|
797
|
+
async refreshSecrets() {
|
|
798
|
+
return resolveCommandSecrets(tool.command, options.env);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
subscriptions.set(subscriptionId, { signal: session.signal, stream });
|
|
804
|
+
void (async () => {
|
|
805
|
+
try {
|
|
806
|
+
for await (const event of stream) {
|
|
807
|
+
await notify({
|
|
808
|
+
subscriptionId,
|
|
809
|
+
type: "data",
|
|
810
|
+
event
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
if (!session.signal.aborted) {
|
|
814
|
+
await notify({
|
|
815
|
+
subscriptionId,
|
|
816
|
+
type: "end"
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
catch (error) {
|
|
821
|
+
if (!session.signal.aborted) {
|
|
822
|
+
await notify({
|
|
823
|
+
subscriptionId,
|
|
824
|
+
type: "error",
|
|
825
|
+
error: error instanceof Error ? error.message : String(error)
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
subscriptions.delete(subscriptionId);
|
|
831
|
+
await stream.cancel();
|
|
832
|
+
}
|
|
833
|
+
})();
|
|
834
|
+
return {
|
|
835
|
+
subscriptionId,
|
|
836
|
+
eventSchema: applySchemaCasing(toJsonSchema(streamDefinition.event), casing)
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
server.method(MCP_STREAM_METHODS.unsubscribe, async (request, session) => {
|
|
840
|
+
const subscriptionId = typeof request?.subscriptionId === "string" ? request.subscriptionId : undefined;
|
|
841
|
+
const subscription = subscriptionId === undefined ? undefined : subscriptions.get(subscriptionId);
|
|
842
|
+
if (subscriptionId === undefined ||
|
|
843
|
+
subscription === undefined ||
|
|
844
|
+
subscription.signal !== session.signal) {
|
|
845
|
+
return { unsubscribed: false };
|
|
846
|
+
}
|
|
847
|
+
await subscription.stream.cancel();
|
|
848
|
+
subscriptions.delete(subscriptionId);
|
|
849
|
+
return { unsubscribed: true };
|
|
850
|
+
});
|
|
851
|
+
for (const tool of tools.filter((candidate) => candidate.command.stream === undefined)) {
|
|
732
852
|
const handler = async (argumentsValue) => {
|
|
733
853
|
let params;
|
|
734
854
|
let secrets;
|
package/dist/sdk.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Group, HandlerFs, LogLevel, RuntimeLoggerInput, Scope } from "./in
|
|
|
3
3
|
import { type ErrorReportsOption } from "./error-report.js";
|
|
4
4
|
import type { HumanInLoopPending, HumanInLoopRuntimeOptions } from "./human-in-loop/index.js";
|
|
5
5
|
import { type ValidationError } from "./validation-errors.js";
|
|
6
|
+
import type { StreamConsumerOptions, ToolcraftStream } from "./stream.js";
|
|
6
7
|
type ScopeInput = readonly Scope[] | undefined;
|
|
7
8
|
type HumanInLoopMode = "sync" | "async";
|
|
8
9
|
type HumanInLoopModeInput = HumanInLoopMode | null | undefined;
|
|
@@ -32,7 +33,7 @@ type Camelize<TValue> = TValue extends Primitive ? TValue : TValue extends reado
|
|
|
32
33
|
[TKey in keyof TValue as TKey extends string ? CamelCase<TKey> : TKey]: Camelize<TValue[TKey]>;
|
|
33
34
|
} : TValue;
|
|
34
35
|
type SDKResult<TResult, THumanInLoopMode extends HumanInLoopMode | undefined> = [THumanInLoopMode] extends ["async"] ? ["async"] extends [THumanInLoopMode] ? HumanInLoopPending : TResult : TResult;
|
|
35
|
-
type SDKMethod<TParamsSchema extends ObjectSchema<any>, TResult> = (params: Camelize<Static<TParamsSchema>>) => Promise<TResult>;
|
|
36
|
+
type SDKMethod<TParamsSchema extends ObjectSchema<any>, TResult> = TResult extends ToolcraftStream<infer TEvent> ? (params: Camelize<Static<TParamsSchema>>, options?: StreamConsumerOptions) => ToolcraftStream<TEvent> : (params: Camelize<Static<TParamsSchema>>) => Promise<TResult>;
|
|
36
37
|
type UnionToIntersection<TValue> = (TValue extends unknown ? (value: TValue) => void : never) extends (value: infer TResult) => void ? TResult : never;
|
|
37
38
|
type Simplify<TValue> = {
|
|
38
39
|
[TKey in keyof TValue]: TValue[TKey];
|
package/dist/sdk.js
CHANGED
|
@@ -10,6 +10,7 @@ import { suggest } from "./suggest.js";
|
|
|
10
10
|
import { throwValidationErrors } from "./validation-errors.js";
|
|
11
11
|
import { createRuntimeLogger } from "./runtime-logging.js";
|
|
12
12
|
import { createEnv, createFs, validateServices } from "./runtime/io.js";
|
|
13
|
+
import { createManagedStream } from "./stream.js";
|
|
13
14
|
function splitWords(value) {
|
|
14
15
|
const words = [];
|
|
15
16
|
let current = "";
|
|
@@ -330,6 +331,61 @@ function createResolvedSDK(root, options = {}) {
|
|
|
330
331
|
if (sdkParamsSchema?.kind === "object") {
|
|
331
332
|
validateUniqueSDKParameterMembers(sdkParamsSchema);
|
|
332
333
|
}
|
|
334
|
+
if (node.stream !== undefined) {
|
|
335
|
+
return (params, streamOptions = {}) => createManagedStream({
|
|
336
|
+
eventSchema: node.stream.event,
|
|
337
|
+
...streamOptions,
|
|
338
|
+
async create(signal, status) {
|
|
339
|
+
const commandPath = [...path, node.name].join(".");
|
|
340
|
+
let secrets;
|
|
341
|
+
let validatedParams;
|
|
342
|
+
try {
|
|
343
|
+
secrets = resolveCommandSecrets(node, options.env);
|
|
344
|
+
const baseContext = {
|
|
345
|
+
...services,
|
|
346
|
+
runtimeOptions,
|
|
347
|
+
root,
|
|
348
|
+
secrets,
|
|
349
|
+
fetch: runtimeFetch,
|
|
350
|
+
fs: createFs(options.fs),
|
|
351
|
+
env: createEnv(options.env),
|
|
352
|
+
diagnostics,
|
|
353
|
+
progress(message) {
|
|
354
|
+
diagnostics.emit({ level: "info", message, category: "progress" });
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
await assertCommandRequirements(node, { ...baseContext, params: undefined }, { apiVersion: options.apiVersion, env: options.env });
|
|
358
|
+
const paramsSchema = filterSchemaForScope(node.params, "sdk");
|
|
359
|
+
if (paramsSchema === undefined || paramsSchema.kind !== "object") {
|
|
360
|
+
throw new ToolcraftBugError(`command "${node.name}" must define an object params schema for SDK.`);
|
|
361
|
+
}
|
|
362
|
+
validatedParams = validateSDKArguments(paramsSchema, params);
|
|
363
|
+
return await node.handler({
|
|
364
|
+
...baseContext,
|
|
365
|
+
params: validatedParams,
|
|
366
|
+
signal,
|
|
367
|
+
status,
|
|
368
|
+
async refreshSecrets() {
|
|
369
|
+
return resolveCommandSecrets(node, options.env);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
await writeErrorReport({
|
|
375
|
+
command: node,
|
|
376
|
+
commandPath,
|
|
377
|
+
env: process.env,
|
|
378
|
+
error,
|
|
379
|
+
errorReports: options.errorReports,
|
|
380
|
+
params: validatedParams,
|
|
381
|
+
projectRoot: options.projectRoot,
|
|
382
|
+
secrets
|
|
383
|
+
});
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
333
389
|
return async (params) => {
|
|
334
390
|
const commandPath = [...path, node.name].join(".");
|
|
335
391
|
let secrets;
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AnySchema, type Static } from "toolcraft-schema";
|
|
2
|
+
export type StreamStatusType = "connected" | "progress" | "reconnecting";
|
|
3
|
+
export interface StreamStatusEvent {
|
|
4
|
+
type: StreamStatusType;
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface StreamConsumerOptions {
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
onStatus?: (event: StreamStatusEvent) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface ToolcraftStream<TEvent> extends AsyncIterable<TEvent> {
|
|
12
|
+
readonly signal: AbortSignal;
|
|
13
|
+
cancel(reason?: unknown): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface ManagedStreamOptions<TSchema extends AnySchema> extends StreamConsumerOptions {
|
|
16
|
+
eventSchema: TSchema;
|
|
17
|
+
create(signal: AbortSignal, status: (event: StreamStatusEvent) => void): Promise<AsyncIterable<Static<TSchema>>>;
|
|
18
|
+
}
|
|
19
|
+
export declare function createManagedStream<TSchema extends AnySchema>(options: ManagedStreamOptions<TSchema>): ToolcraftStream<Static<TSchema>>;
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { validate } from "toolcraft-schema";
|
|
2
|
+
import { UserError } from "./user-error.js";
|
|
3
|
+
function validationMessage(schema, value) {
|
|
4
|
+
const result = validate(schema, value);
|
|
5
|
+
return result.ok ? undefined : result.issues.map((issue) => issue.message).join("; ");
|
|
6
|
+
}
|
|
7
|
+
export function createManagedStream(options) {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
let iterator;
|
|
10
|
+
let iteratorPromise;
|
|
11
|
+
let closePromise;
|
|
12
|
+
let done = false;
|
|
13
|
+
const getIterator = async () => {
|
|
14
|
+
if (iterator !== undefined) {
|
|
15
|
+
return iterator;
|
|
16
|
+
}
|
|
17
|
+
iteratorPromise ??= options
|
|
18
|
+
.create(controller.signal, (event) => options.onStatus?.(event))
|
|
19
|
+
.then((iterable) => iterable[Symbol.asyncIterator]());
|
|
20
|
+
iterator = await iteratorPromise;
|
|
21
|
+
return iterator;
|
|
22
|
+
};
|
|
23
|
+
const close = async (reason) => {
|
|
24
|
+
if (closePromise !== undefined) {
|
|
25
|
+
return closePromise;
|
|
26
|
+
}
|
|
27
|
+
closePromise = (async () => {
|
|
28
|
+
controller.abort(reason);
|
|
29
|
+
options.signal?.removeEventListener("abort", abortFromConsumer);
|
|
30
|
+
if (done || (iterator === undefined && iteratorPromise === undefined)) {
|
|
31
|
+
done = true;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const activeIterator = await getIterator();
|
|
35
|
+
done = true;
|
|
36
|
+
await activeIterator.return?.();
|
|
37
|
+
})();
|
|
38
|
+
return closePromise;
|
|
39
|
+
};
|
|
40
|
+
const abortFromConsumer = () => {
|
|
41
|
+
void close(options.signal?.reason);
|
|
42
|
+
};
|
|
43
|
+
if (options.signal?.aborted === true) {
|
|
44
|
+
abortFromConsumer();
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
options.signal?.addEventListener("abort", abortFromConsumer, { once: true });
|
|
48
|
+
}
|
|
49
|
+
const stream = {
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
cancel: close,
|
|
52
|
+
[Symbol.asyncIterator]() {
|
|
53
|
+
return {
|
|
54
|
+
async next() {
|
|
55
|
+
if (done || controller.signal.aborted) {
|
|
56
|
+
await close(controller.signal.reason);
|
|
57
|
+
return { done: true, value: undefined };
|
|
58
|
+
}
|
|
59
|
+
const activeIterator = await getIterator();
|
|
60
|
+
let result;
|
|
61
|
+
try {
|
|
62
|
+
result = await activeIterator.next();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
await close(error);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
if (result.done === true) {
|
|
69
|
+
done = true;
|
|
70
|
+
options.signal?.removeEventListener("abort", abortFromConsumer);
|
|
71
|
+
return { done: true, value: undefined };
|
|
72
|
+
}
|
|
73
|
+
const message = validationMessage(options.eventSchema, result.value);
|
|
74
|
+
if (message !== undefined) {
|
|
75
|
+
await close(new UserError(message));
|
|
76
|
+
throw new UserError(message);
|
|
77
|
+
}
|
|
78
|
+
return { done: false, value: result.value };
|
|
79
|
+
},
|
|
80
|
+
async return() {
|
|
81
|
+
await close();
|
|
82
|
+
return { done: true, value: undefined };
|
|
83
|
+
},
|
|
84
|
+
async throw(error) {
|
|
85
|
+
await close(error);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
return stream;
|
|
92
|
+
}
|
|
@@ -3,6 +3,7 @@ import { type DiagnosticLogEvent, type LogLevel } from "../runtime-logging.js";
|
|
|
3
3
|
import { type FetchRoute } from "./fakes.js";
|
|
4
4
|
import { type FsChange, type MemoryFs } from "./memory-fs.js";
|
|
5
5
|
import { type ParityResult } from "./parity.js";
|
|
6
|
+
import { type StreamStatusEvent } from "../stream.js";
|
|
6
7
|
export type PipelineStage = "resolve" | "secrets" | "requirements" | "params" | "confirm" | "handler" | "render";
|
|
7
8
|
export type EffectEvent = {
|
|
8
9
|
seq: number;
|
|
@@ -67,10 +68,20 @@ export interface RunResult<T> {
|
|
|
67
68
|
}
|
|
68
69
|
export interface CommandTestHarness {
|
|
69
70
|
run<T>(path: string[], params?: Record<string, unknown>): Promise<RunResult<T>>;
|
|
71
|
+
stream<T>(path: string[], params?: Record<string, unknown>, options?: {
|
|
72
|
+
limit?: number;
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
}): Promise<StreamRunResult<T>>;
|
|
70
75
|
parity(path: string[], params?: Record<string, unknown>): Promise<ParityResult>;
|
|
71
76
|
fs: MemoryFs;
|
|
72
77
|
timeline: EffectEvent[];
|
|
73
78
|
}
|
|
79
|
+
export interface StreamRunResult<T> {
|
|
80
|
+
ok: boolean;
|
|
81
|
+
events: T[];
|
|
82
|
+
statuses: StreamStatusEvent[];
|
|
83
|
+
error?: unknown;
|
|
84
|
+
}
|
|
74
85
|
type EmptyHarnessServices = Record<string, never>;
|
|
75
86
|
export declare function createCommandTestHarness<TServices extends object = EmptyHarnessServices>(root: Group, options?: HarnessOptions<TServices>): CommandTestHarness;
|
|
76
87
|
export {};
|