@botbotgo/agent-harness 0.0.17 → 0.0.19
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 +95 -387
- package/dist/api.d.ts +3 -0
- package/dist/api.js +7 -0
- package/dist/config/agent-context.md +8 -0
- package/dist/config/orchestra.yaml +11 -8
- package/dist/config/{runtime.yaml → workspace.yaml} +23 -0
- package/dist/contracts/types.d.ts +14 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/mcp.d.ts +12 -0
- package/dist/mcp.js +112 -0
- package/dist/resource/isolation.d.ts +2 -0
- package/dist/resource/isolation.js +79 -0
- package/dist/resource/resource-impl.d.ts +18 -0
- package/dist/resource/resource-impl.js +179 -11
- package/dist/resource/sources.d.ts +3 -0
- package/dist/resource/sources.js +105 -25
- package/dist/runtime/checkpoint-maintenance.d.ts +36 -0
- package/dist/runtime/checkpoint-maintenance.js +223 -0
- package/dist/runtime/harness.d.ts +10 -1
- package/dist/runtime/harness.js +38 -2
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/sqlite-maintained-checkpoint-saver.d.ts +9 -0
- package/dist/runtime/sqlite-maintained-checkpoint-saver.js +39 -0
- package/dist/runtime/support/runtime-factories.js +3 -1
- package/dist/tool-modules.d.ts +17 -0
- package/dist/tool-modules.js +143 -0
- package/dist/tools.d.ts +20 -0
- package/dist/tools.js +17 -0
- package/dist/workspace/compile.js +124 -5
- package/dist/workspace/object-loader.js +90 -24
- package/dist/workspace/resource-compilers.d.ts +3 -1
- package/dist/workspace/resource-compilers.js +72 -5
- package/package.json +10 -3
|
@@ -45,3 +45,26 @@ spec:
|
|
|
45
45
|
codebase, choose {{secondaryAgentId}}.
|
|
46
46
|
|
|
47
47
|
When uncertain, prefer {{secondaryAgentId}}.
|
|
48
|
+
|
|
49
|
+
# agent-harness feature: optional workspace-level background maintenance policies.
|
|
50
|
+
# This is the framework-owned place to configure periodic housekeeping that should run without
|
|
51
|
+
# coupling cleanup to user-triggered checkpoint reads or writes.
|
|
52
|
+
#
|
|
53
|
+
# Current support:
|
|
54
|
+
# - checkpoint cleanup for `SqliteSaver` checkpointers only
|
|
55
|
+
# - oldest-first deletion by time policy and/or size policy
|
|
56
|
+
# - background scheduling inside the harness lifecycle
|
|
57
|
+
#
|
|
58
|
+
# Example:
|
|
59
|
+
# maintenance:
|
|
60
|
+
# checkpoints:
|
|
61
|
+
# enabled: true
|
|
62
|
+
# schedule:
|
|
63
|
+
# intervalSeconds: 3600
|
|
64
|
+
# runOnStartup: true
|
|
65
|
+
# policies:
|
|
66
|
+
# maxAgeSeconds: 604800
|
|
67
|
+
# maxBytes: 1073741824
|
|
68
|
+
# sqlite:
|
|
69
|
+
# sweepBatchSize: 200
|
|
70
|
+
# vacuum: false
|
|
@@ -8,6 +8,7 @@ export type ParsedAgentObject = {
|
|
|
8
8
|
modelRef: string;
|
|
9
9
|
runRoot?: string;
|
|
10
10
|
toolRefs: string[];
|
|
11
|
+
mcpServers?: Array<Record<string, unknown>>;
|
|
11
12
|
skillPathRefs: string[];
|
|
12
13
|
memorySources: string[];
|
|
13
14
|
subagentRefs: string[];
|
|
@@ -52,6 +53,18 @@ export type ParsedVectorStoreObject = {
|
|
|
52
53
|
metadata?: Record<string, unknown>;
|
|
53
54
|
sourcePath: string;
|
|
54
55
|
};
|
|
56
|
+
export type ParsedMcpServerObject = {
|
|
57
|
+
id: string;
|
|
58
|
+
transport: "stdio" | "http" | "sse" | "websocket";
|
|
59
|
+
url?: string;
|
|
60
|
+
command?: string;
|
|
61
|
+
args?: string[];
|
|
62
|
+
env?: Record<string, string>;
|
|
63
|
+
cwd?: string;
|
|
64
|
+
token?: string;
|
|
65
|
+
headers?: Record<string, string>;
|
|
66
|
+
sourcePath: string;
|
|
67
|
+
};
|
|
55
68
|
export type ParsedToolObject = {
|
|
56
69
|
id: string;
|
|
57
70
|
type: string;
|
|
@@ -174,6 +187,7 @@ export type WorkspaceBundle = {
|
|
|
174
187
|
models: Map<string, ParsedModelObject>;
|
|
175
188
|
embeddings: Map<string, ParsedEmbeddingModelObject>;
|
|
176
189
|
vectorStores: Map<string, ParsedVectorStoreObject>;
|
|
190
|
+
mcpServers: Map<string, ParsedMcpServerObject>;
|
|
177
191
|
tools: Map<string, ParsedToolObject>;
|
|
178
192
|
agents: Map<string, ParsedAgentObject>;
|
|
179
193
|
bindings: Map<string, CompiledAgentBinding>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
export { createAgentHarness, getThread, run, subscribe, stop } from "./api.js";
|
|
1
|
+
export { createAgentHarness, createToolMcpServer, getThread, run, serveToolsOverStdio, subscribe, stop } from "./api.js";
|
|
2
|
+
export type { ToolMcpServerOptions } from "./mcp.js";
|
|
3
|
+
export { tool } from "./tools.js";
|
package/dist/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { createAgentHarness, getThread, run, subscribe, stop } from "./api.js";
|
|
1
|
+
export { createAgentHarness, createToolMcpServer, getThread, run, serveToolsOverStdio, subscribe, stop } from "./api.js";
|
|
2
|
+
export { tool } from "./tools.js";
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { AgentHarness } from "./runtime/harness.js";
|
|
3
|
+
export type ToolMcpServerOptions = {
|
|
4
|
+
agentId: string;
|
|
5
|
+
serverInfo?: {
|
|
6
|
+
name?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
};
|
|
9
|
+
includeToolNames?: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function createToolMcpServerFromHarness(harness: AgentHarness, options: ToolMcpServerOptions): Promise<McpServer>;
|
|
12
|
+
export declare function serveToolsOverStdioFromHarness(harness: AgentHarness, options: ToolMcpServerOptions): Promise<McpServer>;
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { loadToolModuleDefinition } from "./tool-modules.js";
|
|
6
|
+
function asResolvedTool(value) {
|
|
7
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
8
|
+
}
|
|
9
|
+
async function buildRawShapeForTool(harness, toolId, resolvedTool) {
|
|
10
|
+
const tool = harness.getWorkspace().tools.get(toolId);
|
|
11
|
+
if (!tool) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
if (tool.type === "function") {
|
|
15
|
+
const imported = await import(pathToFileURL(tool.sourcePath).href);
|
|
16
|
+
const definition = loadToolModuleDefinition(imported, tool.implementationName ?? tool.id);
|
|
17
|
+
return typeof definition.schema.shape === "object" && definition.schema.shape !== null
|
|
18
|
+
? definition.schema.shape
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
const typedResolvedTool = asResolvedTool(resolvedTool);
|
|
22
|
+
if (!typedResolvedTool?.inputSchemaPromise) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return jsonSchemaObjectToRawShape(await typedResolvedTool.inputSchemaPromise);
|
|
26
|
+
}
|
|
27
|
+
async function invokeResolvedTool(resolvedTool, input) {
|
|
28
|
+
const typed = asResolvedTool(resolvedTool);
|
|
29
|
+
if (!typed) {
|
|
30
|
+
throw new Error("Resolved tool is not invokable.");
|
|
31
|
+
}
|
|
32
|
+
if (typeof typed.invoke === "function") {
|
|
33
|
+
return typed.invoke(input);
|
|
34
|
+
}
|
|
35
|
+
if (typeof typed.call === "function") {
|
|
36
|
+
return typed.call(input);
|
|
37
|
+
}
|
|
38
|
+
if (typeof typed.func === "function") {
|
|
39
|
+
return typed.func(input);
|
|
40
|
+
}
|
|
41
|
+
throw new Error("Resolved tool does not expose invoke, call, or func.");
|
|
42
|
+
}
|
|
43
|
+
function renderToolOutput(output) {
|
|
44
|
+
if (typeof output === "string") {
|
|
45
|
+
return output;
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify(output, null, 2);
|
|
48
|
+
}
|
|
49
|
+
function jsonSchemaObjectToRawShape(schema) {
|
|
50
|
+
if (!schema || schema.type !== "object") {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const properties = typeof schema.properties === "object" && schema.properties
|
|
54
|
+
? schema.properties
|
|
55
|
+
: {};
|
|
56
|
+
const required = Array.isArray(schema.required)
|
|
57
|
+
? new Set(schema.required.filter((item) => typeof item === "string"))
|
|
58
|
+
: new Set();
|
|
59
|
+
const shape = {};
|
|
60
|
+
for (const [key, propertySchema] of Object.entries(properties)) {
|
|
61
|
+
const built = jsonSchemaToZod(propertySchema);
|
|
62
|
+
shape[key] = required.has(key) ? built : built.optional();
|
|
63
|
+
}
|
|
64
|
+
return shape;
|
|
65
|
+
}
|
|
66
|
+
function jsonSchemaToZod(schema) {
|
|
67
|
+
switch (schema?.type) {
|
|
68
|
+
case "string":
|
|
69
|
+
return z.string();
|
|
70
|
+
case "number":
|
|
71
|
+
return z.number();
|
|
72
|
+
case "integer":
|
|
73
|
+
return z.number().int();
|
|
74
|
+
case "boolean":
|
|
75
|
+
return z.boolean();
|
|
76
|
+
case "array":
|
|
77
|
+
return z.array(jsonSchemaToZod(typeof schema.items === "object" && schema.items ? schema.items : undefined));
|
|
78
|
+
case "object": {
|
|
79
|
+
const shape = jsonSchemaObjectToRawShape(schema);
|
|
80
|
+
return z.object(shape ?? {}).passthrough();
|
|
81
|
+
}
|
|
82
|
+
default:
|
|
83
|
+
return z.any();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function createToolMcpServerFromHarness(harness, options) {
|
|
87
|
+
const server = new McpServer({
|
|
88
|
+
name: options.serverInfo?.name ?? `agent-harness-${options.agentId}`,
|
|
89
|
+
version: options.serverInfo?.version ?? "0.0.16",
|
|
90
|
+
});
|
|
91
|
+
const allowedNames = options.includeToolNames ? new Set(options.includeToolNames) : null;
|
|
92
|
+
for (const { compiledTool, resolvedTool } of harness.resolveAgentTools(options.agentId)) {
|
|
93
|
+
if (allowedNames && !allowedNames.has(compiledTool.name)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const rawShape = await buildRawShapeForTool(harness, compiledTool.id, resolvedTool);
|
|
97
|
+
server.tool(compiledTool.name, compiledTool.description, rawShape ?? {}, async (input) => ({
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: renderToolOutput(await invokeResolvedTool(resolvedTool, input)),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
return server;
|
|
107
|
+
}
|
|
108
|
+
export async function serveToolsOverStdioFromHarness(harness, options) {
|
|
109
|
+
const server = await createToolMcpServerFromHarness(harness, options);
|
|
110
|
+
await server.connect(new StdioServerTransport());
|
|
111
|
+
return server;
|
|
112
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { cp, mkdir, readFile, rm, symlink } from "node:fs/promises";
|
|
7
|
+
const isolatedResourceRootCache = new Map();
|
|
8
|
+
function isolationCacheRoot() {
|
|
9
|
+
return path.join(os.tmpdir(), "agent-harness-resource-isolation");
|
|
10
|
+
}
|
|
11
|
+
function isolationDir(packageRoot) {
|
|
12
|
+
const digest = createHash("sha256").update(packageRoot).digest("hex").slice(0, 16);
|
|
13
|
+
return path.join(isolationCacheRoot(), `${path.basename(packageRoot)}-${digest}`);
|
|
14
|
+
}
|
|
15
|
+
function declaredDependencyNames(manifest) {
|
|
16
|
+
return Array.from(new Set([
|
|
17
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
18
|
+
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
19
|
+
...Object.keys(manifest.peerDependencies ?? {}),
|
|
20
|
+
])).sort();
|
|
21
|
+
}
|
|
22
|
+
function dependencyLinkPath(isolatedRoot, dependencyName) {
|
|
23
|
+
return path.join(isolatedRoot, "node_modules", ...dependencyName.split("/"));
|
|
24
|
+
}
|
|
25
|
+
function dependencyPackageRoot(packageJsonPath) {
|
|
26
|
+
return path.dirname(packageJsonPath);
|
|
27
|
+
}
|
|
28
|
+
function resolveDeclaredDependency(packageRoot, dependencyName) {
|
|
29
|
+
const localPath = path.join(packageRoot, "node_modules", ...dependencyName.split("/"), "package.json");
|
|
30
|
+
if (existsSync(localPath)) {
|
|
31
|
+
return dependencyPackageRoot(localPath);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const requireFromPackage = createRequire(path.join(packageRoot, "package.json"));
|
|
35
|
+
return dependencyPackageRoot(requireFromPackage.resolve(`${dependencyName}/package.json`));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function linkDeclaredDependencies(isolatedRoot, packageRoot, manifest) {
|
|
42
|
+
for (const dependencyName of declaredDependencyNames(manifest)) {
|
|
43
|
+
const resolved = resolveDeclaredDependency(packageRoot, dependencyName);
|
|
44
|
+
if (!resolved) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const targetPath = dependencyLinkPath(isolatedRoot, dependencyName);
|
|
48
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
49
|
+
await symlink(resolved, targetPath, "junction");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function buildIsolatedResourceRoot(packageRoot) {
|
|
53
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
54
|
+
const manifest = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
55
|
+
const isolatedRoot = isolationDir(packageRoot);
|
|
56
|
+
await rm(isolatedRoot, { recursive: true, force: true });
|
|
57
|
+
await mkdir(path.dirname(isolatedRoot), { recursive: true });
|
|
58
|
+
await cp(packageRoot, isolatedRoot, {
|
|
59
|
+
recursive: true,
|
|
60
|
+
force: true,
|
|
61
|
+
filter: (source) => path.basename(source) !== "node_modules",
|
|
62
|
+
});
|
|
63
|
+
await mkdir(path.join(isolatedRoot, "node_modules"), { recursive: true });
|
|
64
|
+
await linkDeclaredDependencies(isolatedRoot, packageRoot, manifest);
|
|
65
|
+
return isolatedRoot;
|
|
66
|
+
}
|
|
67
|
+
export async function ensureIsolatedResourcePackageRoot(packageRoot) {
|
|
68
|
+
const cached = isolatedResourceRootCache.get(packageRoot);
|
|
69
|
+
if (cached) {
|
|
70
|
+
return cached;
|
|
71
|
+
}
|
|
72
|
+
const loading = buildIsolatedResourceRoot(packageRoot);
|
|
73
|
+
isolatedResourceRootCache.set(packageRoot, loading);
|
|
74
|
+
return loading;
|
|
75
|
+
}
|
|
76
|
+
export async function resolveIsolatedResourceModulePath(packageRoot, sourcePath) {
|
|
77
|
+
const isolatedRoot = await ensureIsolatedResourcePackageRoot(packageRoot);
|
|
78
|
+
return path.join(isolatedRoot, path.relative(packageRoot, sourcePath));
|
|
79
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1
2
|
import type { RuntimeAdapterOptions, WorkspaceBundle } from "../contracts/types.js";
|
|
2
3
|
export declare function resolveLocalBuiltinsEntry(currentResourceDir: string, resolveInstalledEntry?: () => string | null): string;
|
|
3
4
|
export type ResourceToolInfo = {
|
|
@@ -10,6 +11,23 @@ export type ResourceToolInfo = {
|
|
|
10
11
|
allow: Array<"approve" | "edit" | "reject">;
|
|
11
12
|
};
|
|
12
13
|
};
|
|
14
|
+
export type McpServerConfig = {
|
|
15
|
+
transport?: "stdio" | "http" | "sse" | "websocket";
|
|
16
|
+
command?: string;
|
|
17
|
+
args?: string[];
|
|
18
|
+
env?: Record<string, string>;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
token?: string;
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
export type McpToolDescriptor = {
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
inputSchema?: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
export declare function getOrCreateMcpClient(config: McpServerConfig): Promise<Client>;
|
|
30
|
+
export declare function listRemoteMcpTools(config: McpServerConfig): Promise<McpToolDescriptor[]>;
|
|
13
31
|
export declare function ensureResourceSources(sources?: string[], workspaceRoot?: string): Promise<void>;
|
|
14
32
|
export declare function defaultResourceSkillsRoot(): string;
|
|
15
33
|
export declare function defaultResourceConfigRoot(): string;
|
|
@@ -4,6 +4,13 @@ import path from "node:path";
|
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
9
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
10
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
11
|
+
import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
|
|
12
|
+
import { isSupportedToolModulePath, loadToolModuleDefinition } from "../tool-modules.js";
|
|
13
|
+
import { resolveIsolatedResourceModulePath } from "./isolation.js";
|
|
7
14
|
import { ensureExternalResourceSource, ensureExternalSource, isExternalSourceLocator, parseExternalSourceLocator } from "./sources.js";
|
|
8
15
|
const resourceDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
16
|
const require = createRequire(import.meta.url);
|
|
@@ -77,6 +84,7 @@ async function findPackageRoot(startPath) {
|
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
const functionToolModuleCache = new Map();
|
|
87
|
+
const mcpClientCache = new Map();
|
|
80
88
|
async function loadFunctionToolModule(tool) {
|
|
81
89
|
const cacheKey = `${tool.sourcePath}:${tool.implementationName ?? tool.id}`;
|
|
82
90
|
const cached = functionToolModuleCache.get(cacheKey);
|
|
@@ -84,24 +92,145 @@ async function loadFunctionToolModule(tool) {
|
|
|
84
92
|
return cached;
|
|
85
93
|
}
|
|
86
94
|
const loading = (async () => {
|
|
87
|
-
const
|
|
95
|
+
const packageRoot = await findPackageRoot(tool.sourcePath);
|
|
96
|
+
const isolatedSourcePath = await resolveIsolatedResourceModulePath(packageRoot, tool.sourcePath);
|
|
97
|
+
const imported = await import(pathToFileURL(isolatedSourcePath).href);
|
|
88
98
|
const implementationName = tool.implementationName ?? tool.id;
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
if (typeof invoke !== "function") {
|
|
92
|
-
throw new Error(`Tool module ${tool.sourcePath} must export function ${implementationName}.`);
|
|
93
|
-
}
|
|
94
|
-
if (!schema?.parse) {
|
|
95
|
-
throw new Error(`Tool module ${tool.sourcePath} must export ${implementationName}Schema as a Zod schema.`);
|
|
96
|
-
}
|
|
97
|
-
return { invoke, schema };
|
|
99
|
+
const loaded = loadToolModuleDefinition(imported, implementationName);
|
|
100
|
+
return { invoke: loaded.invoke, schema: loaded.schema, description: loaded.description };
|
|
98
101
|
})();
|
|
99
102
|
functionToolModuleCache.set(cacheKey, loading);
|
|
100
103
|
return loading;
|
|
101
104
|
}
|
|
105
|
+
function normalizeMcpTransport(value) {
|
|
106
|
+
if (value === "stdio" || value === "http" || value === "sse" || value === "websocket") {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
function readMcpServerConfig(workspace, tool) {
|
|
112
|
+
const mcpConfig = typeof tool.config?.mcp === "object" && tool.config?.mcp
|
|
113
|
+
? tool.config.mcp
|
|
114
|
+
: undefined;
|
|
115
|
+
const serverRef = typeof mcpConfig?.serverRef === "string" ? mcpConfig.serverRef : undefined;
|
|
116
|
+
if (serverRef) {
|
|
117
|
+
const serverId = serverRef.startsWith("mcp/") ? serverRef.slice(4) : serverRef;
|
|
118
|
+
const server = workspace.mcpServers.get(serverId);
|
|
119
|
+
if (!server) {
|
|
120
|
+
throw new Error(`MCP tool ${tool.id} references missing MCP server ${serverRef}`);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
transport: server.transport,
|
|
124
|
+
command: server.command,
|
|
125
|
+
args: server.args,
|
|
126
|
+
env: server.env,
|
|
127
|
+
cwd: server.cwd,
|
|
128
|
+
url: server.url,
|
|
129
|
+
token: server.token,
|
|
130
|
+
headers: server.headers,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const config = typeof tool.config === "object" && tool.config
|
|
134
|
+
? tool.config
|
|
135
|
+
: undefined;
|
|
136
|
+
const mcpServer = typeof config?.mcpServer === "object" && config.mcpServer
|
|
137
|
+
? config.mcpServer
|
|
138
|
+
: config;
|
|
139
|
+
if (!mcpServer) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const envRecord = typeof mcpServer.env === "object" && mcpServer.env
|
|
143
|
+
? Object.fromEntries(Object.entries(mcpServer.env).filter((entry) => typeof entry[1] === "string"))
|
|
144
|
+
: undefined;
|
|
145
|
+
return {
|
|
146
|
+
transport: normalizeMcpTransport(mcpServer.transport) ?? ((typeof mcpServer.url === "string" && mcpServer.url.trim()) ? "http" : "stdio"),
|
|
147
|
+
command: typeof mcpServer.command === "string" ? mcpServer.command.trim() : undefined,
|
|
148
|
+
args: Array.isArray(mcpServer.args) ? mcpServer.args.filter((item) => typeof item === "string") : undefined,
|
|
149
|
+
env: envRecord,
|
|
150
|
+
cwd: typeof mcpServer.cwd === "string" ? mcpServer.cwd : undefined,
|
|
151
|
+
url: typeof mcpServer.url === "string" ? mcpServer.url.trim() : undefined,
|
|
152
|
+
token: typeof mcpServer.token === "string" ? mcpServer.token : undefined,
|
|
153
|
+
headers: typeof mcpServer.headers === "object" && mcpServer.headers
|
|
154
|
+
? Object.fromEntries(Object.entries(mcpServer.headers).filter((entry) => typeof entry[1] === "string"))
|
|
155
|
+
: undefined,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function createMcpCacheKey(config) {
|
|
159
|
+
return JSON.stringify({
|
|
160
|
+
transport: config.transport ?? "stdio",
|
|
161
|
+
command: config.command,
|
|
162
|
+
args: config.args ?? [],
|
|
163
|
+
env: config.env ?? {},
|
|
164
|
+
cwd: config.cwd ?? "",
|
|
165
|
+
url: config.url ?? "",
|
|
166
|
+
token: config.token ?? "",
|
|
167
|
+
headers: config.headers ?? {},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
export async function getOrCreateMcpClient(config) {
|
|
171
|
+
const cacheKey = createMcpCacheKey(config);
|
|
172
|
+
const cached = mcpClientCache.get(cacheKey);
|
|
173
|
+
if (cached) {
|
|
174
|
+
return cached;
|
|
175
|
+
}
|
|
176
|
+
const loading = (async () => {
|
|
177
|
+
const client = new Client({
|
|
178
|
+
name: "agent-harness",
|
|
179
|
+
version: "0.0.16",
|
|
180
|
+
});
|
|
181
|
+
const headers = {
|
|
182
|
+
...(config.headers ?? {}),
|
|
183
|
+
...(config.token ? { Authorization: `Bearer ${config.token}` } : {}),
|
|
184
|
+
};
|
|
185
|
+
const transport = config.transport === "http"
|
|
186
|
+
? new StreamableHTTPClientTransport(new URL(config.url ?? ""), {
|
|
187
|
+
requestInit: Object.keys(headers).length > 0 ? { headers } : undefined,
|
|
188
|
+
})
|
|
189
|
+
: config.transport === "sse"
|
|
190
|
+
? new SSEClientTransport(new URL(config.url ?? ""), {
|
|
191
|
+
requestInit: Object.keys(headers).length > 0 ? { headers } : undefined,
|
|
192
|
+
})
|
|
193
|
+
: config.transport === "websocket"
|
|
194
|
+
? new WebSocketClientTransport(new URL(config.url ?? ""))
|
|
195
|
+
: new StdioClientTransport({
|
|
196
|
+
command: config.command ?? "",
|
|
197
|
+
args: config.args,
|
|
198
|
+
env: config.env,
|
|
199
|
+
cwd: config.cwd,
|
|
200
|
+
});
|
|
201
|
+
await client.connect(transport);
|
|
202
|
+
return client;
|
|
203
|
+
})();
|
|
204
|
+
mcpClientCache.set(cacheKey, loading);
|
|
205
|
+
return loading;
|
|
206
|
+
}
|
|
207
|
+
async function getRemoteMcpToolDescriptor(config, remoteToolName) {
|
|
208
|
+
const client = await getOrCreateMcpClient(config);
|
|
209
|
+
const result = await client.listTools();
|
|
210
|
+
const tool = result.tools.find((item) => typeof item.name === "string" && item.name === remoteToolName);
|
|
211
|
+
if (!tool || typeof tool.name !== "string") {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
name: tool.name,
|
|
216
|
+
description: typeof tool.description === "string" ? tool.description : undefined,
|
|
217
|
+
inputSchema: typeof tool.inputSchema === "object" && tool.inputSchema ? tool.inputSchema : undefined,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
export async function listRemoteMcpTools(config) {
|
|
221
|
+
const client = await getOrCreateMcpClient(config);
|
|
222
|
+
const result = await client.listTools();
|
|
223
|
+
return result.tools
|
|
224
|
+
.filter((tool) => typeof tool.name === "string")
|
|
225
|
+
.map((tool) => ({
|
|
226
|
+
name: tool.name,
|
|
227
|
+
description: typeof tool.description === "string" ? tool.description : undefined,
|
|
228
|
+
inputSchema: typeof tool.inputSchema === "object" && tool.inputSchema ? tool.inputSchema : undefined,
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
102
231
|
function createFunctionToolResolver(workspace) {
|
|
103
232
|
const functionTools = new Map(Array.from(workspace.tools.values())
|
|
104
|
-
.filter((tool) => tool.type === "function" && tool.sourcePath
|
|
233
|
+
.filter((tool) => tool.type === "function" && isSupportedToolModulePath(tool.sourcePath))
|
|
105
234
|
.map((tool) => [tool.id, tool]));
|
|
106
235
|
return (toolIds) => toolIds.flatMap((toolId) => {
|
|
107
236
|
const tool = functionTools.get(toolId);
|
|
@@ -127,6 +256,43 @@ function createFunctionToolResolver(workspace) {
|
|
|
127
256
|
];
|
|
128
257
|
});
|
|
129
258
|
}
|
|
259
|
+
function createMcpToolResolver(workspace) {
|
|
260
|
+
const mcpTools = new Map(Array.from(workspace.tools.values())
|
|
261
|
+
.filter((tool) => tool.type === "mcp")
|
|
262
|
+
.map((tool) => [tool.id, tool]));
|
|
263
|
+
return (toolIds) => toolIds.flatMap((toolId) => {
|
|
264
|
+
const tool = mcpTools.get(toolId);
|
|
265
|
+
if (!tool) {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
const serverConfig = readMcpServerConfig(workspace, tool);
|
|
269
|
+
if (!serverConfig) {
|
|
270
|
+
throw new Error(`MCP tool ${tool.id} must define mcp.serverRef or inline MCP server config.`);
|
|
271
|
+
}
|
|
272
|
+
const remoteToolName = tool.mcpRef ?? tool.name;
|
|
273
|
+
const descriptorPromise = getRemoteMcpToolDescriptor(serverConfig, remoteToolName);
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
name: tool.name,
|
|
277
|
+
description: tool.description,
|
|
278
|
+
inputSchemaPromise: descriptorPromise.then((descriptor) => descriptor?.inputSchema),
|
|
279
|
+
async invoke(input) {
|
|
280
|
+
const client = await getOrCreateMcpClient(serverConfig);
|
|
281
|
+
const result = await client.callTool({
|
|
282
|
+
name: remoteToolName,
|
|
283
|
+
arguments: typeof input === "object" && input !== null ? input : {},
|
|
284
|
+
});
|
|
285
|
+
const textParts = Array.isArray(result.content)
|
|
286
|
+
? result.content
|
|
287
|
+
.filter((item) => typeof item === "object" && item !== null && "type" in item)
|
|
288
|
+
.flatMap((item) => item.type === "text" && typeof item.text === "string" ? [item.text] : [])
|
|
289
|
+
: [];
|
|
290
|
+
return textParts.length > 0 ? textParts.join("\n\n") : JSON.stringify(result);
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
];
|
|
294
|
+
});
|
|
295
|
+
}
|
|
130
296
|
function resolvePackageEntry(packageRoot, pkg) {
|
|
131
297
|
const exportsField = pkg.exports;
|
|
132
298
|
if (typeof exportsField === "string") {
|
|
@@ -222,6 +388,7 @@ export function createResourceBackendResolver(workspace) {
|
|
|
222
388
|
}
|
|
223
389
|
export function createResourceToolResolver(workspace, options = {}) {
|
|
224
390
|
const functionResolver = createFunctionToolResolver(workspace);
|
|
391
|
+
const mcpResolver = createMcpToolResolver(workspace);
|
|
225
392
|
const localResolver = createProviderToolResolver(localResource, workspace, options);
|
|
226
393
|
const remoteResolvers = (workspace.resourceSources ?? workspace.builtinSources ?? [])
|
|
227
394
|
.map((source) => remoteResourceCache.get(source))
|
|
@@ -231,6 +398,7 @@ export function createResourceToolResolver(workspace, options = {}) {
|
|
|
231
398
|
return (toolIds, binding) => {
|
|
232
399
|
const resolved = [
|
|
233
400
|
...functionResolver(toolIds, binding),
|
|
401
|
+
...mcpResolver(toolIds, binding),
|
|
234
402
|
...(localResolver?.(toolIds, binding) ?? []),
|
|
235
403
|
...remoteResolvers.flatMap((resolver) => resolver(toolIds, binding)),
|
|
236
404
|
];
|
|
@@ -2,6 +2,9 @@ type ParsedLocator = {
|
|
|
2
2
|
kind: "npm" | "tgz" | "file";
|
|
3
3
|
spec: string;
|
|
4
4
|
subpath?: string;
|
|
5
|
+
integrity?: string;
|
|
6
|
+
signature?: string;
|
|
7
|
+
publicKey?: string;
|
|
5
8
|
};
|
|
6
9
|
export declare function resolveResourcePackageRoot(root: string): string | null;
|
|
7
10
|
export declare function isExternalSourceLocator(value: unknown): boolean;
|