Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions demo/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import ToolCall from "./tool-call";
import Message from "./message";
import Annotations from "./annotations";
import McpToolsList from "./mcp-tools-list";
import McpApproval from "./mcp-approval";
import { Item, McpApprovalRequestItem } from "@/lib/assistant";
import LoadingMessage from "./loading-message";
Expand Down Expand Up @@ -58,8 +57,6 @@ const Chat: React.FC<ChatProps> = ({ items, onSendMessage, onApprovalResponse })
<Annotations annotations={item.content[0].annotations} />
)}
</div>
) : item.type === "mcp_list_tools" ? (
<McpToolsList item={item} />
) : item.type === "mcp_approval_request" ? (
<McpApproval item={item as McpApprovalRequestItem} onRespond={onApprovalResponse} />
) : null}
Expand Down
22 changes: 1 addition & 21 deletions demo/lib/assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,6 @@ export interface ToolCallItem {
}[];
}

export interface McpListToolsItem {
type: "mcp_list_tools";
id: string;
server_label: string;
tools: { name: string; description?: string }[];
}

export interface McpApprovalRequestItem {
type: "mcp_approval_request";
id: string;
Expand All @@ -61,7 +54,7 @@ export interface McpApprovalRequestItem {
arguments?: string;
}

@frac frac May 27, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Remove it from the demo, importing *.ts is a bad practice anyway.

export type Item = MessageItem | ToolCallItem | McpListToolsItem | McpApprovalRequestItem;
export type Item = MessageItem | ToolCallItem | McpApprovalRequestItem;

export const handleTurn = async (messages: any[], tools: any[], onMessage: (data: any) => void) => {
try {
Expand Down Expand Up @@ -477,19 +470,6 @@ export const processMessages = async () => {
console.log("response completed", data);
const { response } = data;

// Handle MCP tools list
const mcpListToolsMessage = response.output.find((m: Item) => m.type === "mcp_list_tools");

if (mcpListToolsMessage) {
chatMessages.push({
type: "mcp_list_tools",
id: mcpListToolsMessage.id,
server_label: mcpListToolsMessage.server_label,
tools: mcpListToolsMessage.tools || [],
});
setChatMessages([...chatMessages]);
}

// Handle MCP approval request
const mcpApprovalRequestMessage = response.output.find((m: Item) => m.type === "mcp_approval_request");

Expand Down
2 changes: 1 addition & 1 deletion src/routes/responses/closeOutputItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export async function* closeLastOutputItem(
sequence_number: SEQUENCE_NUMBER_PLACEHOLDER,
};
} else if (lastOutputItem?.type === "mcp_list_tools") {
// Already finalized by `listMcpToolsStream`; do not re-emit done.
// Internal MCP tool-list metadata is not client-visible; do not re-emit done.
} else {
throw new StreamingError(
`Not implemented: expected message, function_call, or mcp_call, got ${(lastOutputItem as ResponseOutputItem)?.type}`
Expand Down
2 changes: 1 addition & 1 deletion src/routes/responses/innerStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ vi.mock("./handleOneTurn.js", () => ({

// Mock mcpStream
vi.mock("./mcpStream.js", () => ({
listMcpToolsStream: vi.fn(),
listMcpTools: vi.fn(),
callApprovedMCPToolStream: vi.fn(),
}));

Expand Down
15 changes: 3 additions & 12 deletions src/routes/responses/innerStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { config } from "../../lib/config.js";
import { formatInputToMessages } from "./messageFormatting.js";
import { buildLLMPayload } from "./payloadBuilder.js";
import { handleOneTurnStream } from "./handleOneTurn.js";
import { listMcpToolsStream, callApprovedMCPToolStream } from "./mcpStream.js";
import { listMcpTools, callApprovedMCPToolStream } from "./mcpStream.js";

export async function* innerRunStream(
req: ValidatedRequest<CreateResponseParams>,
Expand Down Expand Up @@ -99,18 +99,9 @@ export async function* innerRunStream(
}
}
}
// Otherwise, list tools from MCP server
// Otherwise, list tools from MCP server for internal orchestration only.
if (!mcpListTools) {
for await (const event of listMcpToolsStream(tool, responseObject, traceContext, log)) {
yield event;
}
const lastOutput = responseObject.output.at(-1);
if (!lastOutput || lastOutput.type !== "mcp_list_tools") {
throw new Error(
`Expected mcp_list_tools output after listMcpToolsStream, got ${lastOutput?.type ?? "undefined"}`
);
}
mcpListTools = lastOutput;
mcpListTools = await listMcpTools(tool, traceContext, log);
}

// Only allowed tools are forwarded to the LLM
Expand Down
28 changes: 12 additions & 16 deletions src/routes/responses/mcpStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ vi.mock("../../mcp.js", () => ({
connectMcpServer: vi.fn(),
}));

import { listMcpToolsStream, callApprovedMCPToolStream } from "./mcpStream.js";
import { listMcpTools, callApprovedMCPToolStream } from "./mcpStream.js";
import { connectMcpServer, callMcpTool } from "../../mcp.js";
import { createMockResponseObject, createMockLogger, collectEvents } from "./__test_helpers__/mocks.js";
import type { McpServerParams } from "../../schemas.js";
Expand All @@ -42,7 +42,7 @@ import type { Logger } from "pino";

const log = createMockLogger() as unknown as Logger;

describe("listMcpToolsStream", () => {
describe("listMcpTools", () => {
const traceContext = {} as Context;
const mcpTool: McpServerParams = {
server_label: "test-server",
Expand All @@ -57,7 +57,7 @@ describe("listMcpToolsStream", () => {
vi.clearAllMocks();
});

it("yields correct event sequence on success", async () => {
it("fetches tools internally without adding public response output or events", async () => {
const mockClient = {
listTools: vi.fn().mockResolvedValue({
tools: [
Expand All @@ -74,24 +74,20 @@ describe("listMcpToolsStream", () => {
(connectMcpServer as ReturnType<typeof vi.fn>).mockResolvedValue(mockClient);

const responseObject = createMockResponseObject();
const events = await collectEvents(listMcpToolsStream(mcpTool, responseObject, traceContext, log));
const types = events.map((e) => e.type);
const result = await listMcpTools(mcpTool, traceContext, log);

expect(types).toEqual([
"response.output_item.added",
"response.mcp_list_tools.in_progress",
"response.mcp_list_tools.completed",
"response.output_item.done",
]);
expect(types.filter((t) => t === "response.output_item.done")).toHaveLength(1);
expect(result).toMatchObject({
type: "mcp_list_tools",
server_label: "test-server",
tools: [{ name: "search", input_schema: { type: "object" }, description: "Search tool" }],
});
expect(responseObject.output).toEqual([]);
});
Comment on lines 74 to 85

it("yields failed event and throws on connection error", async () => {
it("throws on connection error without yielding public failure events", async () => {
(connectMcpServer as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("Connection refused"));

const responseObject = createMockResponseObject();

await expect(collectEvents(listMcpToolsStream(mcpTool, responseObject, traceContext, log))).rejects.toThrow(
await expect(listMcpTools(mcpTool, traceContext, log)).rejects.toThrow(
"Failed to list tools from MCP server 'test-server'"
);
});
Expand Down
39 changes: 3 additions & 36 deletions src/routes/responses/mcpStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ import {
} from "./types.js";
import { buildJsonAttribute, recordError } from "./utils.js";

export async function* listMcpToolsStream(
export async function listMcpTools(
tool: McpServerParams,
responseObject: IncompleteResponse,
traceContext: Context,
log: Logger
): AsyncGenerator<PatchedResponseStreamEvent> {
): Promise<ResponseOutputItem.McpListTools> {
const span = tracer.startSpan(
"gen_ai.execute_tool",
{
Expand All @@ -38,55 +37,23 @@ export async function* listMcpToolsStream(
server_label: tool.server_label,
tools: [],
};
responseObject.output.push(outputObject);

yield {
type: "response.output_item.added",
output_index: responseObject.output.length - 1,
item: outputObject,
sequence_number: SEQUENCE_NUMBER_PLACEHOLDER,
};

yield {
type: "response.mcp_list_tools.in_progress",
item_id: outputObject.id,
output_index: responseObject.output.length - 1,
sequence_number: SEQUENCE_NUMBER_PLACEHOLDER,
};

let mcp: Awaited<ReturnType<typeof connectMcpServer>> | undefined;
try {
mcp = await connectMcpServer(tool, log);
const mcpTools = await mcp.listTools();
yield {
type: "response.mcp_list_tools.completed",
item_id: outputObject.id,
output_index: responseObject.output.length - 1,
sequence_number: SEQUENCE_NUMBER_PLACEHOLDER,
};
outputObject.tools = mcpTools.tools.map((mcpTool) => ({
input_schema: mcpTool.inputSchema,
name: mcpTool.name,
annotations: mcpTool.annotations,
description: mcpTool.description,
}));
span.setAttribute("mcp.tools.count", outputObject.tools.length);
yield {
type: "response.output_item.done",
output_index: responseObject.output.length - 1,
item: outputObject,
sequence_number: SEQUENCE_NUMBER_PLACEHOLDER,
};
return outputObject;
} catch (error) {
const errorMessage = `Failed to list tools from MCP server '${tool.server_label}': ${error instanceof Error ? error.message : "Unknown error"}`;
log.error({ err: error, server_label: tool.server_label }, "Failed to list MCP tools");
recordError(span, error);
yield {
type: "response.mcp_list_tools.failed",
item_id: outputObject.id,
output_index: responseObject.output.length - 1,
sequence_number: SEQUENCE_NUMBER_PLACEHOLDER,
};
throw new Error(errorMessage);
} finally {
if (mcp) {
Expand Down
67 changes: 16 additions & 51 deletions tests/responses.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,23 +497,11 @@ describe("responses.js", function () {
});

assert.ok(Array.isArray(response.output));
assert.ok(response.output.length >= 2);

// Check first output item (mcp_list_tools)
const listToolsOutput = response.output[0];
assert.equal(listToolsOutput.type, "mcp_list_tools");
assert.equal(listToolsOutput.server_label, "gitmcp");
assert.ok(listToolsOutput.id);
assert.ok(Array.isArray(listToolsOutput.tools));
assert.ok(listToolsOutput.tools.length > 0);

// Check that tools array contains expected tools
const toolNames = listToolsOutput.tools.map((tool) => tool.name);
assert.ok(toolNames.includes("fetch_tiktoken_documentation"));
assert.ok(toolNames.includes("search_tiktoken_documentation"));

// Check second output item (mcp_call)
const mcpCallOutput = response.output[1];
assert.ok(response.output.length >= 1);
assert.ok(!response.output.some((item) => item.type === "mcp_list_tools"));

// Check first output item (mcp_call)
const mcpCallOutput = response.output[0];
assert.equal(mcpCallOutput.type, "mcp_call");
assert.equal(mcpCallOutput.name, "fetch_tiktoken_documentation");
assert.equal(mcpCallOutput.server_label, "gitmcp");
Expand Down Expand Up @@ -571,23 +559,11 @@ describe("responses.js", function () {
});

assert.ok(Array.isArray(response.output));
assert.ok(response.output.length === 2);

// Check first output item (mcp_list_tools)
const listToolsOutput = response.output[0];
assert.equal(listToolsOutput.type, "mcp_list_tools");
assert.equal(listToolsOutput.server_label, "gitmcp");
assert.ok(listToolsOutput.id);
assert.ok(Array.isArray(listToolsOutput.tools));
assert.ok(listToolsOutput.tools.length > 0);

// Check that tools array contains expected tools
const toolNames = listToolsOutput.tools.map((tool) => tool.name);
assert.ok(toolNames.includes("fetch_tiktoken_documentation"));
assert.ok(toolNames.includes("search_tiktoken_documentation"));

// Check second output item (mcp_approval_request)
const approvalRequestOutput = response.output[1];
assert.ok(response.output.length === 1);
assert.ok(!response.output.some((item) => item.type === "mcp_list_tools"));

// Check first output item (mcp_approval_request)
const approvalRequestOutput = response.output[0];
assert.equal(approvalRequestOutput.type, "mcp_approval_request");
assert.equal(approvalRequestOutput.name, "fetch_tiktoken_documentation");
assert.equal(approvalRequestOutput.server_label, "gitmcp");
Expand Down Expand Up @@ -616,23 +592,11 @@ describe("responses.js", function () {
});

assert.ok(Array.isArray(response.output));
assert.ok(response.output.length >= 2);

// Check first output item (mcp_list_tools)
const listToolsOutput = response.output[0];
assert.equal(listToolsOutput.type, "mcp_list_tools");
assert.equal(listToolsOutput.server_label, "gitmcp");
assert.ok(listToolsOutput.id);
assert.ok(Array.isArray(listToolsOutput.tools));
assert.ok(listToolsOutput.tools.length > 0);

// Check that tools array contains expected tools
const toolNames = listToolsOutput.tools.map((tool) => tool.name);
assert.ok(toolNames.includes("fetch_tiktoken_documentation"));
assert.ok(toolNames.includes("search_tiktoken_documentation"));

// Check second output item (mcp_call)
const mcpCallOutput = response.output[1];
assert.ok(response.output.length >= 1);
assert.ok(!response.output.some((item) => item.type === "mcp_list_tools"));

// Check first output item (mcp_call)
const mcpCallOutput = response.output[0];
assert.equal(mcpCallOutput.type, "mcp_call");
assert.equal(mcpCallOutput.name, "fetch_tiktoken_documentation");
assert.equal(mcpCallOutput.server_label, "gitmcp");
Expand Down Expand Up @@ -703,6 +667,7 @@ describe("responses.js", function () {

assert.ok(Array.isArray(response.output));
assert.ok(response.output.length === 1);
assert.ok(!response.output.some((item) => item.type === "mcp_list_tools"));

// Check that the first output item is an approval request (not a list_tools call)
const approvalRequestOutput = response.output[0];
Expand Down
Loading