@purista/harness 1.0.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/LICENSE +201 -0
- package/README.md +23 -0
- package/dist/agents/index.d.ts +34 -0
- package/dist/agents/index.js +301 -0
- package/dist/errors/catalog.d.ts +185 -0
- package/dist/errors/catalog.js +144 -0
- package/dist/errors/harness-error.d.ts +64 -0
- package/dist/errors/harness-error.js +58 -0
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors/redaction.d.ts +5 -0
- package/dist/errors/redaction.js +64 -0
- package/dist/harness/defineHarness.d.ts +640 -0
- package/dist/harness/defineHarness.js +176 -0
- package/dist/harness/errors.d.ts +62 -0
- package/dist/harness/errors.js +67 -0
- package/dist/harness/types.d.ts +27 -0
- package/dist/harness/types.js +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.js +2 -0
- package/dist/logger/json-logger.d.ts +31 -0
- package/dist/logger/json-logger.js +65 -0
- package/dist/logger/logger.d.ts +31 -0
- package/dist/logger/logger.js +1 -0
- package/dist/models/json.d.ts +6 -0
- package/dist/models/json.js +1 -0
- package/dist/models/registry.d.ts +112 -0
- package/dist/models/registry.js +286 -0
- package/dist/models/state.d.ts +64 -0
- package/dist/models/state.js +1 -0
- package/dist/ports/base-model-provider.d.ts +56 -0
- package/dist/ports/base-model-provider.js +343 -0
- package/dist/ports/capabilities.d.ts +70 -0
- package/dist/ports/capabilities.js +38 -0
- package/dist/ports/feedback.d.ts +29 -0
- package/dist/ports/feedback.js +1 -0
- package/dist/ports/harness-context.d.ts +20 -0
- package/dist/ports/harness-context.js +1 -0
- package/dist/ports/index.d.ts +6 -0
- package/dist/ports/index.js +6 -0
- package/dist/ports/model-provider.d.ts +280 -0
- package/dist/ports/model-provider.js +1 -0
- package/dist/ports/state.d.ts +72 -0
- package/dist/ports/state.js +24 -0
- package/dist/runtime/durable.d.ts +134 -0
- package/dist/runtime/durable.js +185 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/steps.d.ts +22 -0
- package/dist/runtime/steps.js +51 -0
- package/dist/sandbox/index.d.ts +111 -0
- package/dist/sandbox/index.js +165 -0
- package/dist/sessions/index.d.ts +23 -0
- package/dist/sessions/index.js +718 -0
- package/dist/skills/index.d.ts +8 -0
- package/dist/skills/index.js +88 -0
- package/dist/state/in-memory.d.ts +35 -0
- package/dist/state/in-memory.js +140 -0
- package/dist/telemetry/index.d.ts +1 -0
- package/dist/telemetry/index.js +1 -0
- package/dist/telemetry/shim.d.ts +26 -0
- package/dist/telemetry/shim.js +120 -0
- package/dist/testing/capabilities.d.ts +11 -0
- package/dist/testing/capabilities.js +20 -0
- package/dist/testing/fakeModelProvider.d.ts +25 -0
- package/dist/testing/fakeModelProvider.js +79 -0
- package/dist/testing/feedback.d.ts +10 -0
- package/dist/testing/feedback.js +24 -0
- package/dist/testing/fixtures/mcp/fake-http-server.d.ts +8 -0
- package/dist/testing/fixtures/mcp/fake-http-server.js +95 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/sandboxContract.d.ts +4 -0
- package/dist/testing/sandboxContract.js +74 -0
- package/dist/testing/sandboxSnapshot.d.ts +7 -0
- package/dist/testing/sandboxSnapshot.js +201 -0
- package/dist/testing/stateStoreContract.d.ts +2 -0
- package/dist/testing/stateStoreContract.js +109 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +123 -0
- package/dist/tools/mcp/http.d.ts +2 -0
- package/dist/tools/mcp/http.js +109 -0
- package/dist/tools/mcp/index.d.ts +2 -0
- package/dist/tools/mcp/index.js +2 -0
- package/dist/tools/mcp/runner.d.ts +74 -0
- package/dist/tools/mcp/runner.js +238 -0
- package/dist/tools/mcp/schema.d.ts +41 -0
- package/dist/tools/mcp/schema.js +251 -0
- package/dist/tools/mcp/stdio.d.ts +2 -0
- package/dist/tools/mcp/stdio.js +122 -0
- package/dist/ulid/index.d.ts +6 -0
- package/dist/ulid/index.js +35 -0
- package/dist/workflows/index.d.ts +8 -0
- package/dist/workflows/index.js +26 -0
- package/package.json +75 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { McpAuthError, McpProtocolError, OperationTimeoutError } from '../../errors/index.js';
|
|
2
|
+
import { withMcpTimeout } from './runner.js';
|
|
3
|
+
export function createHttpMcpTransportRunner(config) {
|
|
4
|
+
let connected;
|
|
5
|
+
async function connect(options) {
|
|
6
|
+
connected ??= (async () => {
|
|
7
|
+
const [{ Client }, { StreamableHTTPClientTransport }] = await Promise.all([
|
|
8
|
+
import('@modelcontextprotocol/sdk/client/index.js'),
|
|
9
|
+
import('@modelcontextprotocol/sdk/client/streamableHttp.js')
|
|
10
|
+
]);
|
|
11
|
+
const transport = new StreamableHTTPClientTransport(new URL(config.url), {
|
|
12
|
+
requestInit: { headers: buildHeaders(config.headers, config.auth) }
|
|
13
|
+
});
|
|
14
|
+
const client = new Client({ name: `purista-harness-${config.localToolId}`, version: '0.0.0' });
|
|
15
|
+
try {
|
|
16
|
+
await client.connect(transport, toSdkOptions(options));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
connected = undefined;
|
|
20
|
+
throw mapHttpError(config, 'connect', error);
|
|
21
|
+
}
|
|
22
|
+
return { client, transport };
|
|
23
|
+
})();
|
|
24
|
+
return connected;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
async listTools(options) {
|
|
28
|
+
try {
|
|
29
|
+
const { client } = await connect(options);
|
|
30
|
+
return (await client.listTools(undefined, toSdkOptions(options))).tools;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof McpAuthError || error instanceof McpProtocolError)
|
|
34
|
+
throw error;
|
|
35
|
+
throw mapHttpError(config, 'list', error);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
async callTool(name, input, options) {
|
|
39
|
+
try {
|
|
40
|
+
const { client } = await connect(options);
|
|
41
|
+
return await withMcpTimeout({ ...(options?.signal ? { signal: options.signal } : {}), timeoutMs: options?.timeoutMs ?? config.timeoutMs, scope: 'tool' }, (signal) => client.callTool({ name, arguments: input }, undefined, toSdkOptions({ ...(signal ? { signal } : {}), timeoutMs: options?.timeoutMs ?? config.timeoutMs })));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof McpAuthError || error instanceof McpProtocolError)
|
|
45
|
+
throw error;
|
|
46
|
+
if (error instanceof OperationTimeoutError)
|
|
47
|
+
throw error;
|
|
48
|
+
throw mapHttpError(config, 'call', error);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async close() {
|
|
52
|
+
if (!connected)
|
|
53
|
+
return;
|
|
54
|
+
const current = await connected.catch(() => undefined);
|
|
55
|
+
connected = undefined;
|
|
56
|
+
await current?.transport.close();
|
|
57
|
+
await current?.client.close();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function buildHeaders(headers, auth) {
|
|
62
|
+
const next = { ...(headers ?? {}) };
|
|
63
|
+
if (!auth || auth.kind === 'none')
|
|
64
|
+
return next;
|
|
65
|
+
if (auth.kind === 'bearer')
|
|
66
|
+
next['authorization'] = `Bearer ${auth.token}`;
|
|
67
|
+
if (auth.kind === 'oauth2')
|
|
68
|
+
next['authorization'] = `Bearer ${auth.accessToken}`;
|
|
69
|
+
if (auth.kind === 'api_key')
|
|
70
|
+
next[auth.header] = auth.value;
|
|
71
|
+
if (auth.kind === 'basic')
|
|
72
|
+
next['authorization'] = `Basic ${Buffer.from(`${auth.username}:${auth.password}`).toString('base64')}`;
|
|
73
|
+
return next;
|
|
74
|
+
}
|
|
75
|
+
function toSdkOptions(options) {
|
|
76
|
+
if (!options)
|
|
77
|
+
return undefined;
|
|
78
|
+
return {
|
|
79
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
80
|
+
...(options.timeoutMs ? { timeout: options.timeoutMs } : {})
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function mapHttpError(config, phase, error) {
|
|
84
|
+
const status = statusFromError(error);
|
|
85
|
+
if (status === 401 || status === 403) {
|
|
86
|
+
return new McpAuthError('MCP HTTP authentication failed.', { tool_id: config.localToolId, auth_kind: config.auth?.kind ?? 'none', status }, error);
|
|
87
|
+
}
|
|
88
|
+
return new McpProtocolError('MCP HTTP transport failed.', { tool_id: config.localToolId, transport: 'http', phase }, error);
|
|
89
|
+
}
|
|
90
|
+
function statusFromError(error) {
|
|
91
|
+
if (typeof error === 'object' && error !== null) {
|
|
92
|
+
const maybe = error;
|
|
93
|
+
if (typeof maybe.status === 'number')
|
|
94
|
+
return maybe.status;
|
|
95
|
+
if (typeof maybe.code === 'number')
|
|
96
|
+
return maybe.code;
|
|
97
|
+
const causeStatus = statusFromError(maybe.cause);
|
|
98
|
+
if (causeStatus !== undefined)
|
|
99
|
+
return causeStatus;
|
|
100
|
+
}
|
|
101
|
+
if (error instanceof Error) {
|
|
102
|
+
if (/unauthorized/i.test(error.message))
|
|
103
|
+
return 401;
|
|
104
|
+
const match = /\b(401|403|4\d\d|5\d\d)\b/.exec(error.message);
|
|
105
|
+
if (match?.[1])
|
|
106
|
+
return Number(match[1]);
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { McpHttpToolDefinition, McpStdioToolDefinition, ToolDefinition, ToolsConfig } from '../../harness/defineHarness.js';
|
|
2
|
+
import type { JsonValue } from '../../models/json.js';
|
|
3
|
+
import type { ModelToolSpec } from '../../ports/model-provider.js';
|
|
4
|
+
import type { SandboxSession } from '../../sandbox/index.js';
|
|
5
|
+
import { type McpSchemaWarning } from './schema.js';
|
|
6
|
+
export type McpToolKind = 'mcp_stdio' | 'mcp_http';
|
|
7
|
+
export type McpTransport = 'stdio' | 'http';
|
|
8
|
+
export interface ResolvedMcpTool {
|
|
9
|
+
localToolId: string;
|
|
10
|
+
kind: McpToolKind;
|
|
11
|
+
description: string;
|
|
12
|
+
upstreamToolName: string;
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
serverKey: string;
|
|
15
|
+
inputAdapter?: (input: unknown) => unknown;
|
|
16
|
+
outputAdapter?: (output: unknown) => unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface ResolvedMcpStdioTool extends ResolvedMcpTool {
|
|
19
|
+
kind: 'mcp_stdio';
|
|
20
|
+
command: string;
|
|
21
|
+
args?: readonly string[];
|
|
22
|
+
env?: Record<string, string>;
|
|
23
|
+
install?: McpStdioToolDefinition['install'];
|
|
24
|
+
sandbox: SandboxSession;
|
|
25
|
+
}
|
|
26
|
+
export interface ResolvedMcpHttpTool extends ResolvedMcpTool {
|
|
27
|
+
kind: 'mcp_http';
|
|
28
|
+
url: string;
|
|
29
|
+
auth?: McpHttpToolDefinition['auth'];
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
export type ResolvedMcpToolConfig = ResolvedMcpStdioTool | ResolvedMcpHttpTool;
|
|
33
|
+
export interface McpDiscoveredTool {
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
inputSchema: unknown;
|
|
37
|
+
outputSchema?: unknown;
|
|
38
|
+
}
|
|
39
|
+
export interface McpTransportRunner {
|
|
40
|
+
listTools(options?: {
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
}): Promise<McpDiscoveredTool[]>;
|
|
44
|
+
callTool(name: string, input: unknown, options?: {
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
}): Promise<unknown>;
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export interface McpRunnerRegistry {
|
|
51
|
+
getRunner(config: ResolvedMcpToolConfig): McpTransportRunner;
|
|
52
|
+
close(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export interface McpFacadeContext {
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
toolTimeoutMs?: number;
|
|
57
|
+
sandbox?: SandboxSession;
|
|
58
|
+
sandboxKey?: string;
|
|
59
|
+
registry?: McpRunnerRegistry;
|
|
60
|
+
warn?: (warning: McpSchemaWarning) => void;
|
|
61
|
+
}
|
|
62
|
+
export declare function getMcpToolSpecs(tools: ToolsConfig, allowlist: Iterable<string>, ctx?: McpFacadeContext): Promise<ModelToolSpec[]>;
|
|
63
|
+
export declare function invokeMcpTool(toolId: string, tool: ToolDefinition, input: unknown, ctx: McpFacadeContext): Promise<JsonValue>;
|
|
64
|
+
export declare function invokeMcpTool(config: ResolvedMcpToolConfig, runner: McpTransportRunner, input: unknown, signal?: AbortSignal): Promise<JsonValue>;
|
|
65
|
+
export declare function getModelToolSpec(config: ResolvedMcpToolConfig, runner: McpTransportRunner, signal?: AbortSignal): Promise<ModelToolSpec>;
|
|
66
|
+
export declare function createMcpRunnerRegistry(): McpRunnerRegistry;
|
|
67
|
+
export declare function normalizeMcpOutput(result: unknown): JsonValue;
|
|
68
|
+
export declare function toMcpTransport(kind: McpToolKind): McpTransport;
|
|
69
|
+
export declare function isMcpToolDefinition(tool: ToolDefinition | McpTransportRunner): tool is McpStdioToolDefinition | McpHttpToolDefinition;
|
|
70
|
+
export declare function withMcpTimeout<T>(opts: {
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
timeoutMs?: number;
|
|
73
|
+
scope: 'tool';
|
|
74
|
+
}, fn: (signal?: AbortSignal) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { McpProtocolError, OperationCancelledError, OperationTimeoutError, ToolError, ToolNotFoundError, ValidationError } from '../../errors/index.js';
|
|
2
|
+
import { assertMcpJsonSchema, validateMcpJsonSchema } from './schema.js';
|
|
3
|
+
const discoveredCache = new WeakMap();
|
|
4
|
+
export async function getMcpToolSpecs(tools, allowlist, ctx = {}) {
|
|
5
|
+
const allowed = new Set(allowlist);
|
|
6
|
+
const specs = [];
|
|
7
|
+
const registry = ctx.registry ?? createMcpRunnerRegistry();
|
|
8
|
+
for (const [toolId, tool] of Object.entries(tools)) {
|
|
9
|
+
if (!allowed.has(toolId) || !isMcpToolDefinition(tool))
|
|
10
|
+
continue;
|
|
11
|
+
const config = resolveMcpTool(toolId, tool, ctx);
|
|
12
|
+
specs.push(await getResolvedModelToolSpec(config, registry.getRunner(config), ctx.signal, ctx.warn));
|
|
13
|
+
}
|
|
14
|
+
return specs;
|
|
15
|
+
}
|
|
16
|
+
export async function invokeMcpTool(first, second, input, fourth) {
|
|
17
|
+
if (typeof first === 'string') {
|
|
18
|
+
if (!isMcpToolDefinition(second))
|
|
19
|
+
throw new ToolNotFoundError('Tool is not an MCP tool.', { tool_id: first, where: 'registry' });
|
|
20
|
+
const ctx = isAbortSignal(fourth) ? { signal: fourth } : fourth ?? {};
|
|
21
|
+
const registry = ctx.registry ?? createMcpRunnerRegistry();
|
|
22
|
+
const config = resolveMcpTool(first, second, ctx);
|
|
23
|
+
return invokeResolvedMcpTool(config, registry.getRunner(config), input, ctx.signal, ctx.warn);
|
|
24
|
+
}
|
|
25
|
+
return invokeResolvedMcpTool(first, second, input, isAbortSignal(fourth) ? fourth : fourth?.signal, isAbortSignal(fourth) ? undefined : fourth?.warn);
|
|
26
|
+
}
|
|
27
|
+
export async function getModelToolSpec(config, runner, signal) {
|
|
28
|
+
return getResolvedModelToolSpec(config, runner, signal);
|
|
29
|
+
}
|
|
30
|
+
export function createMcpRunnerRegistry() {
|
|
31
|
+
const runners = new Map();
|
|
32
|
+
return {
|
|
33
|
+
getRunner(config) {
|
|
34
|
+
const existing = runners.get(config.localToolId);
|
|
35
|
+
if (existing)
|
|
36
|
+
return existing;
|
|
37
|
+
const runner = config.kind === 'mcp_stdio'
|
|
38
|
+
? createDynamicStdioRunner(config)
|
|
39
|
+
: createDynamicHttpRunner(config);
|
|
40
|
+
runners.set(config.localToolId, runner);
|
|
41
|
+
return runner;
|
|
42
|
+
},
|
|
43
|
+
async close() {
|
|
44
|
+
await Promise.all([...runners.values()].map((runner) => runner.close()));
|
|
45
|
+
runners.clear();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function normalizeMcpOutput(result) {
|
|
50
|
+
if (isRecord(result) && isJsonValue(result.structuredContent))
|
|
51
|
+
return result.structuredContent;
|
|
52
|
+
if (!isRecord(result) || !Array.isArray(result.content))
|
|
53
|
+
return isJsonValue(result) ? result : null;
|
|
54
|
+
const normalized = result.content.map(normalizeContentBlock);
|
|
55
|
+
if (normalized.length === 0)
|
|
56
|
+
return null;
|
|
57
|
+
if (normalized.every((item) => typeof item === 'string'))
|
|
58
|
+
return normalized.join('\n');
|
|
59
|
+
if (normalized.length === 1)
|
|
60
|
+
return normalized[0] ?? null;
|
|
61
|
+
return { content: normalized.filter(isJsonValue) };
|
|
62
|
+
}
|
|
63
|
+
export function toMcpTransport(kind) {
|
|
64
|
+
return kind === 'mcp_stdio' ? 'stdio' : 'http';
|
|
65
|
+
}
|
|
66
|
+
async function getResolvedModelToolSpec(config, runner, signal, warn) {
|
|
67
|
+
const tool = await discoverConfiguredTool(config, runner, signal, warn);
|
|
68
|
+
const description = tool.description ? `${config.description}\n\n${tool.description}` : config.description;
|
|
69
|
+
return { name: config.localToolId, description, parameters: tool.inputSchema };
|
|
70
|
+
}
|
|
71
|
+
async function invokeResolvedMcpTool(config, runner, input, signal, warn) {
|
|
72
|
+
const tool = await discoverConfiguredTool(config, runner, signal, warn);
|
|
73
|
+
const adaptedInput = config.inputAdapter ? config.inputAdapter(input) : input;
|
|
74
|
+
const validatedInput = validateMcpJsonSchema({ toolId: config.localToolId, where: 'mcp_input', schema: tool.inputSchema, value: adaptedInput, ...(warn ? { warn } : {}) });
|
|
75
|
+
const result = await runner.callTool(config.upstreamToolName, validatedInput, { ...(signal ? { signal } : {}), timeoutMs: config.timeoutMs });
|
|
76
|
+
if (isRecord(result) && result.isError === true) {
|
|
77
|
+
throw new ToolError('MCP tool returned an error.', { tool_id: config.localToolId, tool_kind: config.kind });
|
|
78
|
+
}
|
|
79
|
+
const normalized = normalizeMcpOutput(result);
|
|
80
|
+
const validatedOutput = tool.outputSchema
|
|
81
|
+
? validateMcpJsonSchema({ toolId: config.localToolId, where: 'mcp_output', schema: tool.outputSchema, value: normalized, ...(warn ? { warn } : {}) })
|
|
82
|
+
: normalized;
|
|
83
|
+
const adaptedOutput = config.outputAdapter ? config.outputAdapter(validatedOutput) : validatedOutput;
|
|
84
|
+
if (!isJsonValue(adaptedOutput)) {
|
|
85
|
+
throw new ValidationError('MCP output adapter returned a non-JSON value.', { where: 'mcp_output', issues: [{ path: '', message: 'Value must be JSON serializable.' }] });
|
|
86
|
+
}
|
|
87
|
+
return adaptedOutput;
|
|
88
|
+
}
|
|
89
|
+
async function discoverConfiguredTool(config, runner, signal, warn) {
|
|
90
|
+
let promise = discoveredCache.get(runner);
|
|
91
|
+
if (!promise) {
|
|
92
|
+
promise = runner.listTools({ ...(signal ? { signal } : {}), timeoutMs: config.timeoutMs });
|
|
93
|
+
discoveredCache.set(runner, promise);
|
|
94
|
+
}
|
|
95
|
+
const tools = await promise;
|
|
96
|
+
const tool = tools.find((candidate) => candidate.name === config.upstreamToolName);
|
|
97
|
+
if (!tool)
|
|
98
|
+
throw new ToolNotFoundError('MCP upstream tool was not found.', { tool_id: config.localToolId, where: 'registry' });
|
|
99
|
+
try {
|
|
100
|
+
assertMcpJsonSchema(config.localToolId, tool.inputSchema, 'mcp_input', warn);
|
|
101
|
+
if (tool.outputSchema !== undefined)
|
|
102
|
+
assertMcpJsonSchema(config.localToolId, tool.outputSchema, 'mcp_output', warn);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error instanceof ValidationError)
|
|
106
|
+
throw new McpProtocolError('MCP tool schema is malformed.', { tool_id: config.localToolId, transport: toMcpTransport(config.kind), phase: 'list' }, error);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
return tool;
|
|
110
|
+
}
|
|
111
|
+
function resolveMcpTool(toolId, tool, ctx) {
|
|
112
|
+
const base = {
|
|
113
|
+
localToolId: toolId,
|
|
114
|
+
description: tool.description,
|
|
115
|
+
upstreamToolName: tool.tool,
|
|
116
|
+
timeoutMs: ctx.toolTimeoutMs ?? 120_000,
|
|
117
|
+
serverKey: toolId,
|
|
118
|
+
...(tool.inputAdapter ? { inputAdapter: tool.inputAdapter } : {}),
|
|
119
|
+
...(tool.outputAdapter ? { outputAdapter: tool.outputAdapter } : {})
|
|
120
|
+
};
|
|
121
|
+
if (tool.kind === 'mcp_stdio') {
|
|
122
|
+
if (!ctx.sandbox)
|
|
123
|
+
throw new ToolNotFoundError('MCP stdio tool requires a sandbox session.', { tool_id: toolId, where: 'registry' });
|
|
124
|
+
return {
|
|
125
|
+
...base,
|
|
126
|
+
kind: 'mcp_stdio',
|
|
127
|
+
serverKey: `${toolId}:${ctx.sandboxKey ?? 'sandbox'}`,
|
|
128
|
+
command: tool.command,
|
|
129
|
+
...(tool.args ? { args: tool.args } : {}),
|
|
130
|
+
...(tool.env ? { env: tool.env } : {}),
|
|
131
|
+
...(tool.install ? { install: tool.install } : {}),
|
|
132
|
+
sandbox: ctx.sandbox
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
...base,
|
|
137
|
+
kind: 'mcp_http',
|
|
138
|
+
url: tool.url,
|
|
139
|
+
...(tool.auth ? { auth: tool.auth } : {}),
|
|
140
|
+
...(tool.headers ? { headers: tool.headers } : {})
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export function isMcpToolDefinition(tool) {
|
|
144
|
+
return isRecord(tool) && (tool.kind === 'mcp_stdio' || tool.kind === 'mcp_http');
|
|
145
|
+
}
|
|
146
|
+
function createDynamicStdioRunner(config) {
|
|
147
|
+
let runnerPromise;
|
|
148
|
+
return dynamicRunner(() => {
|
|
149
|
+
runnerPromise ??= import('./stdio.js').then((module) => module.createStdioMcpTransportRunner(config));
|
|
150
|
+
return runnerPromise;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function createDynamicHttpRunner(config) {
|
|
154
|
+
let runnerPromise;
|
|
155
|
+
return dynamicRunner(() => {
|
|
156
|
+
runnerPromise ??= import('./http.js').then((module) => module.createHttpMcpTransportRunner(config));
|
|
157
|
+
return runnerPromise;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function dynamicRunner(load) {
|
|
161
|
+
return {
|
|
162
|
+
async listTools(options) { return (await load()).listTools(options); },
|
|
163
|
+
async callTool(name, input, options) { return (await load()).callTool(name, input, options); },
|
|
164
|
+
async close() { await (await load()).close(); }
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function normalizeContentBlock(block) {
|
|
168
|
+
if (!isRecord(block))
|
|
169
|
+
return null;
|
|
170
|
+
if (block.type === 'text' && typeof block.text === 'string')
|
|
171
|
+
return block.text;
|
|
172
|
+
if ((block.type === 'image' || block.type === 'audio') && typeof block.mimeType === 'string') {
|
|
173
|
+
return { contentType: block.mimeType, ...(typeof block.data === 'string' ? { data: block.data } : {}) };
|
|
174
|
+
}
|
|
175
|
+
if (block.type === 'resource' && isRecord(block.resource)) {
|
|
176
|
+
const resource = block.resource;
|
|
177
|
+
return {
|
|
178
|
+
...(typeof resource.mimeType === 'string' ? { contentType: resource.mimeType } : {}),
|
|
179
|
+
...(typeof resource.uri === 'string' ? { uri: resource.uri } : {}),
|
|
180
|
+
...(typeof resource.text === 'string' ? { data: resource.text } : {}),
|
|
181
|
+
...(typeof resource.blob === 'string' ? { data: resource.blob } : {})
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (block.type === 'resource_link') {
|
|
185
|
+
return {
|
|
186
|
+
...(typeof block.mimeType === 'string' ? { contentType: block.mimeType } : {}),
|
|
187
|
+
...(typeof block.uri === 'string' ? { uri: block.uri } : {})
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return isJsonValue(block) ? block : null;
|
|
191
|
+
}
|
|
192
|
+
export async function withMcpTimeout(opts, fn) {
|
|
193
|
+
opts.signal?.throwIfAborted();
|
|
194
|
+
if (!opts.timeoutMs || opts.timeoutMs <= 0)
|
|
195
|
+
return fn(opts.signal);
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
const relay = () => controller.abort(opts.signal?.reason);
|
|
198
|
+
opts.signal?.addEventListener('abort', relay, { once: true });
|
|
199
|
+
let timeoutId;
|
|
200
|
+
const timeout = new Promise((_, reject) => {
|
|
201
|
+
timeoutId = setTimeout(() => {
|
|
202
|
+
const error = new OperationTimeoutError('MCP tool operation timed out.', { scope: opts.scope, timeout_ms: opts.timeoutMs });
|
|
203
|
+
controller.abort(error);
|
|
204
|
+
reject(error);
|
|
205
|
+
}, opts.timeoutMs);
|
|
206
|
+
});
|
|
207
|
+
try {
|
|
208
|
+
return await Promise.race([fn(controller.signal), timeout]);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (controller.signal.aborted && !(controller.signal.reason instanceof OperationTimeoutError)) {
|
|
212
|
+
throw new OperationCancelledError('MCP tool operation was cancelled.', { scope: 'tool' }, controller.signal.reason ?? error);
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
if (timeoutId)
|
|
218
|
+
clearTimeout(timeoutId);
|
|
219
|
+
opts.signal?.removeEventListener('abort', relay);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function isAbortSignal(value) {
|
|
223
|
+
return isRecord(value) && typeof value.aborted === 'boolean' && typeof value.addEventListener === 'function';
|
|
224
|
+
}
|
|
225
|
+
function isRecord(value) {
|
|
226
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
227
|
+
}
|
|
228
|
+
function isJsonValue(value) {
|
|
229
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
230
|
+
return true;
|
|
231
|
+
if (typeof value === 'number')
|
|
232
|
+
return Number.isFinite(value);
|
|
233
|
+
if (Array.isArray(value))
|
|
234
|
+
return value.every(isJsonValue);
|
|
235
|
+
if (isRecord(value))
|
|
236
|
+
return Object.values(value).every(isJsonValue);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { JsonValue } from '../../models/json.js';
|
|
2
|
+
export type McpSchemaValidationWhere = 'mcp_input' | 'mcp_output';
|
|
3
|
+
export interface McpSchemaWarning {
|
|
4
|
+
toolId: string;
|
|
5
|
+
keyword: string;
|
|
6
|
+
path: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ValidateMcpJsonSchemaOptions {
|
|
10
|
+
toolId: string;
|
|
11
|
+
where: McpSchemaValidationWhere;
|
|
12
|
+
schema: unknown;
|
|
13
|
+
value: unknown;
|
|
14
|
+
warn?: (warning: McpSchemaWarning) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function validateMcpJsonSchema(opts: ValidateMcpJsonSchemaOptions): JsonValue;
|
|
17
|
+
export declare function assertMcpJsonSchema(toolId: string, schema: unknown, where: McpSchemaValidationWhere, warn?: (warning: McpSchemaWarning) => void): asserts schema is JsonSchema;
|
|
18
|
+
type JsonSchema = {
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
$schema?: unknown;
|
|
21
|
+
additionalProperties?: unknown;
|
|
22
|
+
allOf?: unknown;
|
|
23
|
+
anyOf?: unknown;
|
|
24
|
+
const?: unknown;
|
|
25
|
+
enum?: unknown;
|
|
26
|
+
format?: unknown;
|
|
27
|
+
items?: unknown;
|
|
28
|
+
maximum?: unknown;
|
|
29
|
+
maxItems?: unknown;
|
|
30
|
+
maxLength?: unknown;
|
|
31
|
+
minimum?: unknown;
|
|
32
|
+
minItems?: unknown;
|
|
33
|
+
minLength?: unknown;
|
|
34
|
+
not?: unknown;
|
|
35
|
+
oneOf?: unknown;
|
|
36
|
+
pattern?: unknown;
|
|
37
|
+
properties?: unknown;
|
|
38
|
+
required?: unknown;
|
|
39
|
+
type?: unknown;
|
|
40
|
+
};
|
|
41
|
+
export {};
|