@posthog/agent 2.1.150 → 2.1.156
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/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +2 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js +10 -3
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
- package/dist/agent.js +59 -6
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.js +1 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.d.ts +36 -0
- package/dist/server/agent-server.js +112 -12
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +138 -14
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +3 -3
- package/src/adapters/claude/conversion/sdk-to-acp.ts +66 -0
- package/src/adapters/claude/conversion/tool-use-to-acp.ts +33 -5
- package/src/server/agent-server.ts +61 -6
- package/src/server/bin.ts +29 -0
- package/src/server/schemas.test.ts +117 -0
- package/src/server/schemas.ts +16 -0
- package/src/server/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@posthog/agent",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.156",
|
|
4
4
|
"repository": "https://github.com/PostHog/twig",
|
|
5
5
|
"description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
|
|
6
6
|
"exports": {
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"tsx": "^4.20.6",
|
|
75
75
|
"typescript": "^5.5.0",
|
|
76
76
|
"vitest": "^2.1.8",
|
|
77
|
-
"@
|
|
78
|
-
"@
|
|
77
|
+
"@posthog/shared": "1.0.0",
|
|
78
|
+
"@twig/git": "1.0.0"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@agentclientprotocol/sdk": "^0.14.0",
|
|
@@ -192,6 +192,7 @@ function handleToolUseChunk(
|
|
|
192
192
|
const toolInfo = toolInfoFromToolUse(chunk, {
|
|
193
193
|
supportsTerminalOutput: ctx.supportsTerminalOutput,
|
|
194
194
|
toolUseId: chunk.id,
|
|
195
|
+
cachedFileContent: ctx.fileContentCache,
|
|
195
196
|
});
|
|
196
197
|
|
|
197
198
|
const meta: Record<string, unknown> = {
|
|
@@ -221,6 +222,66 @@ function handleToolUseChunk(
|
|
|
221
222
|
};
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
function extractTextFromContent(content: unknown): string | null {
|
|
226
|
+
if (Array.isArray(content)) {
|
|
227
|
+
const parts: string[] = [];
|
|
228
|
+
for (const item of content) {
|
|
229
|
+
if (
|
|
230
|
+
typeof item === "object" &&
|
|
231
|
+
item !== null &&
|
|
232
|
+
"text" in item &&
|
|
233
|
+
typeof (item as Record<string, unknown>).text === "string"
|
|
234
|
+
) {
|
|
235
|
+
parts.push((item as { text: string }).text);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return parts.length > 0 ? parts.join("") : null;
|
|
239
|
+
}
|
|
240
|
+
if (typeof content === "string") {
|
|
241
|
+
return content;
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function stripCatLineNumbers(text: string): string {
|
|
247
|
+
return text.replace(/^ *\d+[\t→]/gm, "");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function updateFileContentCache(
|
|
251
|
+
toolUse: { name: string; input: unknown },
|
|
252
|
+
chunk: { content?: unknown },
|
|
253
|
+
ctx: ChunkHandlerContext,
|
|
254
|
+
): void {
|
|
255
|
+
const input = toolUse.input as Record<string, unknown> | undefined;
|
|
256
|
+
const filePath = input?.file_path ? String(input.file_path) : undefined;
|
|
257
|
+
if (!filePath) return;
|
|
258
|
+
|
|
259
|
+
if (toolUse.name === "Read" && !input?.limit && !input?.offset) {
|
|
260
|
+
const fileText = extractTextFromContent(chunk.content);
|
|
261
|
+
if (fileText !== null) {
|
|
262
|
+
ctx.fileContentCache[filePath] = stripCatLineNumbers(fileText);
|
|
263
|
+
}
|
|
264
|
+
} else if (toolUse.name === "Write") {
|
|
265
|
+
const content = input?.content;
|
|
266
|
+
if (typeof content === "string") {
|
|
267
|
+
ctx.fileContentCache[filePath] = content;
|
|
268
|
+
}
|
|
269
|
+
} else if (toolUse.name === "Edit") {
|
|
270
|
+
const oldString = input?.old_string;
|
|
271
|
+
const newString = input?.new_string;
|
|
272
|
+
if (
|
|
273
|
+
typeof oldString === "string" &&
|
|
274
|
+
typeof newString === "string" &&
|
|
275
|
+
filePath in ctx.fileContentCache
|
|
276
|
+
) {
|
|
277
|
+
const current = ctx.fileContentCache[filePath];
|
|
278
|
+
ctx.fileContentCache[filePath] = input?.replace_all
|
|
279
|
+
? current.replaceAll(oldString, newString)
|
|
280
|
+
: current.replace(oldString, newString);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
224
285
|
function handleToolResultChunk(
|
|
225
286
|
chunk: AnthropicContentChunk & {
|
|
226
287
|
tool_use_id: string;
|
|
@@ -241,12 +302,17 @@ function handleToolResultChunk(
|
|
|
241
302
|
return [];
|
|
242
303
|
}
|
|
243
304
|
|
|
305
|
+
if (!chunk.is_error) {
|
|
306
|
+
updateFileContentCache(toolUse, chunk, ctx);
|
|
307
|
+
}
|
|
308
|
+
|
|
244
309
|
const { _meta: resultMeta, ...toolUpdate } = toolUpdateFromToolResult(
|
|
245
310
|
chunk as Parameters<typeof toolUpdateFromToolResult>[0],
|
|
246
311
|
toolUse,
|
|
247
312
|
{
|
|
248
313
|
supportsTerminalOutput: ctx.supportsTerminalOutput,
|
|
249
314
|
toolUseId: chunk.tool_use_id,
|
|
315
|
+
cachedFileContent: ctx.fileContentCache,
|
|
250
316
|
},
|
|
251
317
|
);
|
|
252
318
|
|
|
@@ -34,7 +34,11 @@ type ToolInfo = Pick<ToolCall, "title" | "kind" | "content" | "locations">;
|
|
|
34
34
|
|
|
35
35
|
export function toolInfoFromToolUse(
|
|
36
36
|
toolUse: Pick<ToolUseBlock, "name" | "input">,
|
|
37
|
-
options?: {
|
|
37
|
+
options?: {
|
|
38
|
+
supportsTerminalOutput?: boolean;
|
|
39
|
+
toolUseId?: string;
|
|
40
|
+
cachedFileContent?: Record<string, string>;
|
|
41
|
+
},
|
|
38
42
|
): ToolInfo {
|
|
39
43
|
const name = toolUse.name;
|
|
40
44
|
const input = toolUse.input as Record<string, unknown> | undefined;
|
|
@@ -144,8 +148,24 @@ export function toolInfoFromToolUse(
|
|
|
144
148
|
|
|
145
149
|
case "Edit": {
|
|
146
150
|
const path = input?.file_path ? String(input.file_path) : undefined;
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
let oldText: string | null = input?.old_string
|
|
152
|
+
? String(input.old_string)
|
|
153
|
+
: null;
|
|
154
|
+
let newText: string = input?.new_string ? String(input.new_string) : "";
|
|
155
|
+
|
|
156
|
+
// If we have cached file content, show a full-file diff
|
|
157
|
+
if (
|
|
158
|
+
path &&
|
|
159
|
+
options?.cachedFileContent &&
|
|
160
|
+
path in options.cachedFileContent
|
|
161
|
+
) {
|
|
162
|
+
const oldContent = options.cachedFileContent[path];
|
|
163
|
+
const newContent = input?.replace_all
|
|
164
|
+
? oldContent.replaceAll(oldText ?? "", newText)
|
|
165
|
+
: oldContent.replace(oldText ?? "", newText);
|
|
166
|
+
oldText = oldContent;
|
|
167
|
+
newText = newContent;
|
|
168
|
+
}
|
|
149
169
|
|
|
150
170
|
return {
|
|
151
171
|
title: path ? `Edit \`${path}\`` : "Edit",
|
|
@@ -170,8 +190,12 @@ export function toolInfoFromToolUse(
|
|
|
170
190
|
const filePath = input?.file_path ? String(input.file_path) : undefined;
|
|
171
191
|
const contentStr = input?.content ? String(input.content) : undefined;
|
|
172
192
|
if (filePath) {
|
|
193
|
+
const oldContent =
|
|
194
|
+
options?.cachedFileContent && filePath in options.cachedFileContent
|
|
195
|
+
? options.cachedFileContent[filePath]
|
|
196
|
+
: null;
|
|
173
197
|
contentResult = toolContent()
|
|
174
|
-
.diff(filePath,
|
|
198
|
+
.diff(filePath, oldContent, contentStr ?? "")
|
|
175
199
|
.build();
|
|
176
200
|
} else if (contentStr) {
|
|
177
201
|
contentResult = toolContent().text(contentStr).build();
|
|
@@ -453,7 +477,11 @@ export function toolUpdateFromToolResult(
|
|
|
453
477
|
| BetaRequestMCPToolResultBlockParam
|
|
454
478
|
| BetaToolSearchToolResultBlockParam,
|
|
455
479
|
toolUse: Pick<ToolUseBlock, "name" | "input"> | undefined,
|
|
456
|
-
options?: {
|
|
480
|
+
options?: {
|
|
481
|
+
supportsTerminalOutput?: boolean;
|
|
482
|
+
toolUseId?: string;
|
|
483
|
+
cachedFileContent?: Record<string, string>;
|
|
484
|
+
},
|
|
457
485
|
): Pick<ToolCallUpdate, "title" | "content" | "locations" | "_meta"> {
|
|
458
486
|
if (
|
|
459
487
|
"is_error" in toolResult &&
|
|
@@ -17,6 +17,7 @@ import { TreeTracker } from "../tree-tracker.js";
|
|
|
17
17
|
import type {
|
|
18
18
|
AgentMode,
|
|
19
19
|
DeviceInfo,
|
|
20
|
+
LogLevel,
|
|
20
21
|
TaskRun,
|
|
21
22
|
TreeSnapshotEvent,
|
|
22
23
|
} from "../types.js";
|
|
@@ -155,6 +156,35 @@ export class AgentServer {
|
|
|
155
156
|
private questionRelayedToSlack = false;
|
|
156
157
|
private detectedPrUrl: string | null = null;
|
|
157
158
|
|
|
159
|
+
private emitConsoleLog = (
|
|
160
|
+
level: LogLevel,
|
|
161
|
+
_scope: string,
|
|
162
|
+
message: string,
|
|
163
|
+
data?: unknown,
|
|
164
|
+
): void => {
|
|
165
|
+
if (!this.session) return;
|
|
166
|
+
|
|
167
|
+
const formatted =
|
|
168
|
+
data !== undefined ? `${message} ${JSON.stringify(data)}` : message;
|
|
169
|
+
|
|
170
|
+
const notification = {
|
|
171
|
+
jsonrpc: "2.0",
|
|
172
|
+
method: POSTHOG_NOTIFICATIONS.CONSOLE,
|
|
173
|
+
params: { level, message: formatted },
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
this.broadcastEvent({
|
|
177
|
+
type: "notification",
|
|
178
|
+
timestamp: new Date().toISOString(),
|
|
179
|
+
notification,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
this.session.logWriter.appendRawLine(
|
|
183
|
+
this.session.payload.run_id,
|
|
184
|
+
JSON.stringify(notification),
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
158
188
|
constructor(config: AgentServerConfig) {
|
|
159
189
|
this.config = config;
|
|
160
190
|
this.logger = new Logger({ debug: true, prefix: "[AgentServer]" });
|
|
@@ -565,7 +595,7 @@ export class AgentServer {
|
|
|
565
595
|
|
|
566
596
|
const sessionResponse = await clientConnection.newSession({
|
|
567
597
|
cwd: this.config.repositoryPath,
|
|
568
|
-
mcpServers: [],
|
|
598
|
+
mcpServers: this.config.mcpServers ?? [],
|
|
569
599
|
_meta: {
|
|
570
600
|
sessionId: payload.run_id,
|
|
571
601
|
taskRunId: payload.run_id,
|
|
@@ -590,6 +620,17 @@ export class AgentServer {
|
|
|
590
620
|
logWriter,
|
|
591
621
|
};
|
|
592
622
|
|
|
623
|
+
this.logger = new Logger({
|
|
624
|
+
debug: true,
|
|
625
|
+
prefix: "[AgentServer]",
|
|
626
|
+
onLog: (level, scope, message, data) => {
|
|
627
|
+
// Preserve console output (onLog suppresses default console.*)
|
|
628
|
+
const _formatted =
|
|
629
|
+
data !== undefined ? `${message} ${JSON.stringify(data)}` : message;
|
|
630
|
+
this.emitConsoleLog(level, scope, message, data);
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
|
|
593
634
|
this.logger.info("Session initialized successfully");
|
|
594
635
|
|
|
595
636
|
// Signal in_progress so the UI can start polling for updates
|
|
@@ -1103,15 +1144,29 @@ Important:
|
|
|
1103
1144
|
...snapshot,
|
|
1104
1145
|
device: this.session.deviceInfo,
|
|
1105
1146
|
};
|
|
1147
|
+
|
|
1148
|
+
const notification = {
|
|
1149
|
+
jsonrpc: "2.0" as const,
|
|
1150
|
+
method: POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT,
|
|
1151
|
+
params: snapshotWithDevice,
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1106
1154
|
this.broadcastEvent({
|
|
1107
1155
|
type: "notification",
|
|
1108
1156
|
timestamp: new Date().toISOString(),
|
|
1109
|
-
notification
|
|
1110
|
-
jsonrpc: "2.0",
|
|
1111
|
-
method: POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT,
|
|
1112
|
-
params: snapshotWithDevice,
|
|
1113
|
-
},
|
|
1157
|
+
notification,
|
|
1114
1158
|
});
|
|
1159
|
+
|
|
1160
|
+
// Persist to log writer so cloud runs have tree snapshots
|
|
1161
|
+
const { archiveUrl: _, ...paramsWithoutArchive } = snapshotWithDevice;
|
|
1162
|
+
const logNotification = {
|
|
1163
|
+
...notification,
|
|
1164
|
+
params: paramsWithoutArchive,
|
|
1165
|
+
};
|
|
1166
|
+
this.session.logWriter.appendRawLine(
|
|
1167
|
+
this.session.payload.run_id,
|
|
1168
|
+
JSON.stringify(logNotification),
|
|
1169
|
+
);
|
|
1115
1170
|
}
|
|
1116
1171
|
} catch (error) {
|
|
1117
1172
|
this.logger.error("Failed to capture tree state", error);
|
package/src/server/bin.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { AgentServer } from "./agent-server.js";
|
|
5
|
+
import { mcpServersSchema } from "./schemas.js";
|
|
5
6
|
|
|
6
7
|
const envSchema = z.object({
|
|
7
8
|
JWT_PUBLIC_KEY: z
|
|
@@ -45,6 +46,10 @@ program
|
|
|
45
46
|
.requiredOption("--repositoryPath <path>", "Path to the repository")
|
|
46
47
|
.requiredOption("--taskId <id>", "Task ID")
|
|
47
48
|
.requiredOption("--runId <id>", "Task run ID")
|
|
49
|
+
.option(
|
|
50
|
+
"--mcpServers <json>",
|
|
51
|
+
"MCP servers config as JSON array (ACP McpServer[] format)",
|
|
52
|
+
)
|
|
48
53
|
.action(async (options) => {
|
|
49
54
|
const envResult = envSchema.safeParse(process.env);
|
|
50
55
|
|
|
@@ -60,6 +65,29 @@ program
|
|
|
60
65
|
|
|
61
66
|
const mode = options.mode === "background" ? "background" : "interactive";
|
|
62
67
|
|
|
68
|
+
let mcpServers: z.infer<typeof mcpServersSchema> | undefined;
|
|
69
|
+
if (options.mcpServers) {
|
|
70
|
+
let parsed: unknown;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(options.mcpServers);
|
|
73
|
+
} catch {
|
|
74
|
+
program.error("--mcpServers must be valid JSON");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = mcpServersSchema.safeParse(parsed);
|
|
79
|
+
if (!result.success) {
|
|
80
|
+
const errors = result.error.issues
|
|
81
|
+
.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`)
|
|
82
|
+
.join("\n");
|
|
83
|
+
program.error(
|
|
84
|
+
`--mcpServers validation failed (only remote http/sse servers are supported):\n${errors}`,
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
mcpServers = result.data;
|
|
89
|
+
}
|
|
90
|
+
|
|
63
91
|
const server = new AgentServer({
|
|
64
92
|
port: parseInt(options.port, 10),
|
|
65
93
|
jwtPublicKey: env.JWT_PUBLIC_KEY,
|
|
@@ -70,6 +98,7 @@ program
|
|
|
70
98
|
mode,
|
|
71
99
|
taskId: options.taskId,
|
|
72
100
|
runId: options.runId,
|
|
101
|
+
mcpServers,
|
|
73
102
|
});
|
|
74
103
|
|
|
75
104
|
process.on("SIGINT", async () => {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mcpServersSchema } from "./schemas.js";
|
|
3
|
+
|
|
4
|
+
describe("mcpServersSchema", () => {
|
|
5
|
+
it("accepts a valid HTTP server", () => {
|
|
6
|
+
const result = mcpServersSchema.safeParse([
|
|
7
|
+
{
|
|
8
|
+
type: "http",
|
|
9
|
+
name: "my-server",
|
|
10
|
+
url: "https://mcp.example.com",
|
|
11
|
+
headers: [{ name: "Authorization", value: "Bearer tok" }],
|
|
12
|
+
},
|
|
13
|
+
]);
|
|
14
|
+
expect(result.success).toBe(true);
|
|
15
|
+
expect(result.data).toEqual([
|
|
16
|
+
{
|
|
17
|
+
type: "http",
|
|
18
|
+
name: "my-server",
|
|
19
|
+
url: "https://mcp.example.com",
|
|
20
|
+
headers: [{ name: "Authorization", value: "Bearer tok" }],
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("accepts a valid SSE server", () => {
|
|
26
|
+
const result = mcpServersSchema.safeParse([
|
|
27
|
+
{
|
|
28
|
+
type: "sse",
|
|
29
|
+
name: "sse-server",
|
|
30
|
+
url: "https://sse.example.com/events",
|
|
31
|
+
headers: [],
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
expect(result.success).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("defaults headers to empty array when omitted", () => {
|
|
38
|
+
const result = mcpServersSchema.safeParse([
|
|
39
|
+
{ type: "http", name: "no-headers", url: "https://example.com" },
|
|
40
|
+
]);
|
|
41
|
+
expect(result.success).toBe(true);
|
|
42
|
+
expect(result.data?.[0].headers).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("accepts multiple servers", () => {
|
|
46
|
+
const result = mcpServersSchema.safeParse([
|
|
47
|
+
{ type: "http", name: "a", url: "https://a.com" },
|
|
48
|
+
{ type: "sse", name: "b", url: "https://b.com" },
|
|
49
|
+
]);
|
|
50
|
+
expect(result.success).toBe(true);
|
|
51
|
+
expect(result.data).toHaveLength(2);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("accepts an empty array", () => {
|
|
55
|
+
const result = mcpServersSchema.safeParse([]);
|
|
56
|
+
expect(result.success).toBe(true);
|
|
57
|
+
expect(result.data).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("rejects stdio servers", () => {
|
|
61
|
+
const result = mcpServersSchema.safeParse([
|
|
62
|
+
{
|
|
63
|
+
type: "stdio",
|
|
64
|
+
name: "local",
|
|
65
|
+
command: "/usr/bin/mcp",
|
|
66
|
+
args: [],
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
expect(result.success).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects servers with no type", () => {
|
|
73
|
+
const result = mcpServersSchema.safeParse([
|
|
74
|
+
{ name: "missing-type", url: "https://example.com" },
|
|
75
|
+
]);
|
|
76
|
+
expect(result.success).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("rejects servers with empty name", () => {
|
|
80
|
+
const result = mcpServersSchema.safeParse([
|
|
81
|
+
{ type: "http", name: "", url: "https://example.com" },
|
|
82
|
+
]);
|
|
83
|
+
expect(result.success).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("rejects servers with invalid url", () => {
|
|
87
|
+
const result = mcpServersSchema.safeParse([
|
|
88
|
+
{ type: "http", name: "bad-url", url: "not-a-url" },
|
|
89
|
+
]);
|
|
90
|
+
expect(result.success).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("rejects servers with missing url", () => {
|
|
94
|
+
const result = mcpServersSchema.safeParse([
|
|
95
|
+
{ type: "http", name: "no-url" },
|
|
96
|
+
]);
|
|
97
|
+
expect(result.success).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("rejects non-array input", () => {
|
|
101
|
+
expect(mcpServersSchema.safeParse("not-array").success).toBe(false);
|
|
102
|
+
expect(mcpServersSchema.safeParse({}).success).toBe(false);
|
|
103
|
+
expect(mcpServersSchema.safeParse(null).success).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("rejects headers with missing fields", () => {
|
|
107
|
+
const result = mcpServersSchema.safeParse([
|
|
108
|
+
{
|
|
109
|
+
type: "http",
|
|
110
|
+
name: "bad-headers",
|
|
111
|
+
url: "https://example.com",
|
|
112
|
+
headers: [{ name: "X-Key" }],
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
expect(result.success).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/server/schemas.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
const httpHeaderSchema = z.object({
|
|
4
|
+
name: z.string(),
|
|
5
|
+
value: z.string(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const remoteMcpServerSchema = z.object({
|
|
9
|
+
type: z.enum(["http", "sse"]),
|
|
10
|
+
name: z.string().min(1, "MCP server name is required"),
|
|
11
|
+
url: z.string().url("MCP server url must be a valid URL"),
|
|
12
|
+
headers: z.array(httpHeaderSchema).default([]),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const mcpServersSchema = z.array(remoteMcpServerSchema);
|
|
16
|
+
|
|
17
|
+
export type RemoteMcpServer = z.infer<typeof remoteMcpServerSchema>;
|
|
18
|
+
|
|
3
19
|
export const jsonRpcRequestSchema = z.object({
|
|
4
20
|
jsonrpc: z.literal("2.0"),
|
|
5
21
|
method: z.string(),
|
package/src/server/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentMode } from "../types.js";
|
|
2
|
+
import type { RemoteMcpServer } from "./schemas.js";
|
|
2
3
|
|
|
3
4
|
export interface AgentServerConfig {
|
|
4
5
|
port: number;
|
|
@@ -11,4 +12,5 @@ export interface AgentServerConfig {
|
|
|
11
12
|
taskId: string;
|
|
12
13
|
runId: string;
|
|
13
14
|
version?: string;
|
|
15
|
+
mcpServers?: RemoteMcpServer[];
|
|
14
16
|
}
|