From 11f55c8ee620bd486bb6f156d77828e670bad36f Mon Sep 17 00:00:00 2001 From: Markus Ecker Date: Fri, 5 Dec 2025 11:45:52 +0100 Subject: [PATCH 1/3] Add MCPAppsMiddleware --- middlewares/mcp-apps-middleware/.gitignore | 138 ++ middlewares/mcp-apps-middleware/.npmignore | 12 + middlewares/mcp-apps-middleware/README.md | 58 + .../__tests__/mcp-apps-middleware.test.ts | 1276 +++++++++++++++++ .../__tests__/test-utils.ts | 404 ++++++ middlewares/mcp-apps-middleware/package.json | 37 + middlewares/mcp-apps-middleware/src/index.ts | 612 ++++++++ middlewares/mcp-apps-middleware/tsconfig.json | 24 + .../mcp-apps-middleware/tsup.config.ts | 11 + .../mcp-apps-middleware/vitest.config.ts | 12 + pnpm-lock.yaml | 727 ++++++++-- 11 files changed, 3177 insertions(+), 134 deletions(-) create mode 100644 middlewares/mcp-apps-middleware/.gitignore create mode 100644 middlewares/mcp-apps-middleware/.npmignore create mode 100644 middlewares/mcp-apps-middleware/README.md create mode 100644 middlewares/mcp-apps-middleware/__tests__/mcp-apps-middleware.test.ts create mode 100644 middlewares/mcp-apps-middleware/__tests__/test-utils.ts create mode 100644 middlewares/mcp-apps-middleware/package.json create mode 100644 middlewares/mcp-apps-middleware/src/index.ts create mode 100644 middlewares/mcp-apps-middleware/tsconfig.json create mode 100644 middlewares/mcp-apps-middleware/tsup.config.ts create mode 100644 middlewares/mcp-apps-middleware/vitest.config.ts diff --git a/middlewares/mcp-apps-middleware/.gitignore b/middlewares/mcp-apps-middleware/.gitignore new file mode 100644 index 000000000..39e0a6d8f --- /dev/null +++ b/middlewares/mcp-apps-middleware/.gitignore @@ -0,0 +1,138 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist +.output + +# Gatsby files +.cache/ + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ diff --git a/middlewares/mcp-apps-middleware/.npmignore b/middlewares/mcp-apps-middleware/.npmignore new file mode 100644 index 000000000..aaacf1596 --- /dev/null +++ b/middlewares/mcp-apps-middleware/.npmignore @@ -0,0 +1,12 @@ +.turbo +.DS_Store +.git +.gitignore +.idea +.vscode +.env +__tests__ +src +tsup.config.ts +tsconfig.json +jest.config.js diff --git a/middlewares/mcp-apps-middleware/README.md b/middlewares/mcp-apps-middleware/README.md new file mode 100644 index 000000000..18b6b6255 --- /dev/null +++ b/middlewares/mcp-apps-middleware/README.md @@ -0,0 +1,58 @@ +# @ag-ui/mcp-apps-middleware + +MCP Apps middleware for AG-UI that enables UI-enabled tools from MCP (Model Context Protocol) servers. + +## Installation + +```bash +npm install @ag-ui/mcp-apps-middleware +# or +pnpm add @ag-ui/mcp-apps-middleware +``` + +## Usage + +```typescript +import { MCPAppsMiddleware } from "@ag-ui/mcp-apps-middleware"; + +const agent = new YourAgent().use( + new MCPAppsMiddleware({ + mcpServers: [ + { type: "http", url: "http://localhost:3001/mcp" } + ], + }) +); +``` + +## Features + +- Discovers UI-enabled tools from MCP servers +- Injects tools into the agent's tool list +- Executes tool calls and fetches UI resources +- Emits activity snapshots for rendering MCP Apps UI + +## Configuration + +```typescript +interface MCPAppsMiddlewareConfig { + mcpServers?: MCPClientConfig[]; +} + +type MCPClientConfig = + | { type: "http"; url: string } + | { type: "sse"; url: string; headers?: Record }; +``` + +## Activity Type + +The middleware emits activity snapshots with type `"mcp-apps"`. You can use the exported constant: + +```typescript +import { MCPAppsActivityType } from "@ag-ui/mcp-apps-middleware"; + +// MCPAppsActivityType === "mcp-apps" +``` + +## License + +MIT diff --git a/middlewares/mcp-apps-middleware/__tests__/mcp-apps-middleware.test.ts b/middlewares/mcp-apps-middleware/__tests__/mcp-apps-middleware.test.ts new file mode 100644 index 000000000..9a2275b4e --- /dev/null +++ b/middlewares/mcp-apps-middleware/__tests__/mcp-apps-middleware.test.ts @@ -0,0 +1,1276 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { EventType, BaseEvent } from "@ag-ui/client"; +import { + MCPAppsMiddleware, + MCPClientConfig, + ProxiedMCPRequest, + MCPAppsActivityType, +} from "../src/index"; +import { + MockAgent, + AsyncMockAgent, + ErrorMockAgent, + createRunAgentInput, + createRunStartedEvent, + createRunFinishedEvent, + createTextMessageStartEvent, + createTextMessageContentEvent, + createTextMessageEndEvent, + createMCPToolWithUI, + createMCPToolWithoutUI, + createMCPToolWithEmptyMeta, + createAssistantMessageWithToolCalls, + createToolResultMessage, + createAGUITool, + collectEvents, + createMCPToolCallResult, + createMCPResourceResult, +} from "./test-utils"; + +// Create mock functions that will be referenced in the mock factory +const mockConnect = vi.fn(); +const mockClose = vi.fn(); +const mockListTools = vi.fn(); +const mockCallTool = vi.fn(); +const mockReadResource = vi.fn(); +const mockNotification = vi.fn(); +const mockPing = vi.fn(); + +// Track Client constructor calls +const mockClientConstructorCalls: Array<{ clientInfo: unknown; options: unknown }> = []; + +// Mock the MCP SDK modules - using factory that returns a function returning our mock +vi.mock("@modelcontextprotocol/sdk/client/index.js", () => { + return { + Client: class MockClient { + connect = mockConnect; + close = mockClose; + listTools = mockListTools; + callTool = mockCallTool; + readResource = mockReadResource; + notification = mockNotification; + ping = mockPing; + + constructor(clientInfo: unknown, options: unknown) { + mockClientConstructorCalls.push({ clientInfo, options }); + } + }, + }; +}); + +vi.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({ + SSEClientTransport: vi.fn().mockImplementation(() => ({ type: "sse" })), +})); + +vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ + StreamableHTTPClientTransport: vi.fn().mockImplementation(() => ({ type: "http" })), +})); + +// Mock crypto.randomUUID +vi.mock("crypto", () => ({ + randomUUID: vi.fn(() => `mock-uuid-${Math.random().toString(36).substr(2, 9)}`), +})); + +describe("MCPAppsMiddleware", () => { + beforeEach(() => { + // Reset all mocks before each test + vi.clearAllMocks(); + + // Clear constructor calls tracking + mockClientConstructorCalls.length = 0; + + // Set default mock implementations + mockConnect.mockResolvedValue(undefined); + mockClose.mockResolvedValue(undefined); + mockListTools.mockResolvedValue({ tools: [] }); + mockCallTool.mockResolvedValue({ content: [] }); + mockReadResource.mockResolvedValue({ contents: [] }); + mockNotification.mockResolvedValue(undefined); + mockPing.mockResolvedValue({}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + // ============================================================================= + // 1. Constructor & Configuration Tests + // ============================================================================= + describe("Constructor & Configuration", () => { + it("creates instance with empty config", () => { + const middleware = new MCPAppsMiddleware(); + expect(middleware).toBeInstanceOf(MCPAppsMiddleware); + }); + + it("creates instance with empty object config", () => { + const middleware = new MCPAppsMiddleware({}); + expect(middleware).toBeInstanceOf(MCPAppsMiddleware); + }); + + it("creates instance with HTTP server config", () => { + const config = { + mcpServers: [{ type: "http" as const, url: "http://localhost:3000" }], + }; + const middleware = new MCPAppsMiddleware(config); + expect(middleware).toBeInstanceOf(MCPAppsMiddleware); + }); + + it("creates instance with SSE server config", () => { + const config = { + mcpServers: [{ type: "sse" as const, url: "http://localhost:3000/sse" }], + }; + const middleware = new MCPAppsMiddleware(config); + expect(middleware).toBeInstanceOf(MCPAppsMiddleware); + }); + + it("creates instance with SSE server config including headers", () => { + const config = { + mcpServers: [ + { + type: "sse" as const, + url: "http://localhost:3000/sse", + headers: { Authorization: "Bearer token" }, + }, + ], + }; + const middleware = new MCPAppsMiddleware(config); + expect(middleware).toBeInstanceOf(MCPAppsMiddleware); + }); + + it("creates instance with multiple server configs", () => { + const config = { + mcpServers: [ + { type: "http" as const, url: "http://localhost:3001" }, + { type: "sse" as const, url: "http://localhost:3002/sse" }, + ], + }; + const middleware = new MCPAppsMiddleware(config); + expect(middleware).toBeInstanceOf(MCPAppsMiddleware); + }); + }); + + // ============================================================================= + // 2. Pass-Through Behavior (No MCP Servers) + // ============================================================================= + describe("Pass-Through Behavior (No MCP Servers)", () => { + it("passes through when mcpServers is empty array", async () => { + const middleware = new MCPAppsMiddleware({ mcpServers: [] }); + const agent = new MockAgent([ + createRunStartedEvent(), + createRunFinishedEvent(), + ]); + + const events = await collectEvents(middleware.run(createRunAgentInput(), agent)); + + expect(events).toHaveLength(2); + expect(events[0].type).toBe(EventType.RUN_STARTED); + expect(events[1].type).toBe(EventType.RUN_FINISHED); + }); + + it("passes through when mcpServers is undefined", async () => { + const middleware = new MCPAppsMiddleware({}); + const agent = new MockAgent([ + createRunStartedEvent(), + createTextMessageStartEvent(), + createTextMessageContentEvent(), + createTextMessageEndEvent(), + createRunFinishedEvent(), + ]); + + const events = await collectEvents(middleware.run(createRunAgentInput(), agent)); + + expect(events.length).toBeGreaterThanOrEqual(2); + expect(events[0].type).toBe(EventType.RUN_STARTED); + expect(events[events.length - 1].type).toBe(EventType.RUN_FINISHED); + }); + + it("events flow through unchanged when no servers configured", async () => { + const middleware = new MCPAppsMiddleware(); + const inputEvents = [ + createRunStartedEvent("run-1", "thread-1"), + createTextMessageStartEvent("msg-1"), + createTextMessageContentEvent("msg-1", "Hello World"), + createTextMessageEndEvent("msg-1"), + createRunFinishedEvent("run-1", "thread-1", { success: true }), + ]; + const agent = new MockAgent(inputEvents); + + const events = await collectEvents(middleware.run(createRunAgentInput(), agent)); + + // The middleware uses runNextWithState which transforms chunks + expect(events.length).toBeGreaterThanOrEqual(2); + expect(events[0].type).toBe(EventType.RUN_STARTED); + }); + + it("observable completes correctly with no servers", async () => { + const middleware = new MCPAppsMiddleware(); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + let completed = false; + await new Promise((resolve) => { + middleware.run(createRunAgentInput(), agent).subscribe({ + complete: () => { + completed = true; + resolve(); + }, + }); + }); + + expect(completed).toBe(true); + }); + + it("error propagation works with no servers", async () => { + const middleware = new MCPAppsMiddleware(); + const testError = new Error("Test error"); + const agent = new ErrorMockAgent(testError); + + let caughtError: Error | null = null; + await new Promise((resolve) => { + middleware.run(createRunAgentInput(), agent).subscribe({ + error: (err) => { + caughtError = err; + resolve(); + }, + complete: () => resolve(), + }); + }); + + expect(caughtError).toBe(testError); + }); + }); + + // ============================================================================= + // 3. Tool Discovery Tests + // ============================================================================= + describe("Tool Discovery", () => { + const httpServerConfig: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + const sseServerConfig: MCPClientConfig = { type: "sse", url: "http://localhost:3001/sse" }; + + it("connects to MCP server with correct capabilities", async () => { + mockListTools.mockResolvedValue({ tools: [] }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + await collectEvents(middleware.run(createRunAgentInput(), agent)); + + expect(mockClientConstructorCalls).toHaveLength(1); + expect(mockClientConstructorCalls[0].clientInfo).toEqual({ + name: "mcp-apps-middleware", + version: "1.0.0", + }); + expect(mockClientConstructorCalls[0].options).toMatchObject({ + capabilities: { + extensions: { + "io.modelcontextprotocol/ui": { + mimeTypes: ["text/html+mcp"], + }, + }, + }, + }); + }); + + it("calls listTools on connected client", async () => { + mockListTools.mockResolvedValue({ tools: [] }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + await collectEvents(middleware.run(createRunAgentInput(), agent)); + + expect(mockConnect).toHaveBeenCalled(); + expect(mockListTools).toHaveBeenCalled(); + }); + + it("filters tools by ui/resourceUri presence", async () => { + mockListTools.mockResolvedValue({ + tools: [ + createMCPToolWithUI("ui-tool", "ui://server/dashboard"), + createMCPToolWithoutUI("non-ui-tool"), + createMCPToolWithEmptyMeta("meta-but-no-ui"), + ], + }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + await collectEvents(middleware.run(createRunAgentInput(), agent)); + + // Agent should receive enhanced input with only the UI tool + expect(agent.runCalls).toHaveLength(1); + const enhancedTools = agent.runCalls[0].tools; + expect(enhancedTools).toHaveLength(1); + expect(enhancedTools[0].name).toBe("ui-tool"); + }); + + it("converts MCP tools to AG-UI Tool format correctly", async () => { + mockListTools.mockResolvedValue({ + tools: [ + { + name: "test-tool", + description: "Test tool description", + inputSchema: { type: "object", properties: { foo: { type: "string" } } }, + _meta: { "ui/resourceUri": "ui://server/test" }, + }, + ], + }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + await collectEvents(middleware.run(createRunAgentInput(), agent)); + + const enhancedTools = agent.runCalls[0].tools; + expect(enhancedTools[0].name).toBe("test-tool"); + expect(enhancedTools[0].description).toContain("Test tool description"); + expect(enhancedTools[0].parameters).toEqual({ + type: "object", + properties: { foo: { type: "string" } }, + }); + }); + + it("stores ui/resourceUri in description", async () => { + mockListTools.mockResolvedValue({ + tools: [createMCPToolWithUI("ui-tool", "ui://server/dashboard", "Original description")], + }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + await collectEvents(middleware.run(createRunAgentInput(), agent)); + + const enhancedTools = agent.runCalls[0].tools; + expect(enhancedTools[0].description).toContain("Original description"); + expect(enhancedTools[0].description).toContain("[UI Resource: ui://server/dashboard]"); + }); + + it("handles tools without _meta", async () => { + mockListTools.mockResolvedValue({ + tools: [createMCPToolWithoutUI("no-meta-tool")], + }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + await collectEvents(middleware.run(createRunAgentInput(), agent)); + + // No UI tools should be added + expect(agent.runCalls[0].tools).toHaveLength(0); + }); + + it("handles empty tools list from server", async () => { + mockListTools.mockResolvedValue({ tools: [] }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + await collectEvents(middleware.run(createRunAgentInput(), agent)); + + expect(agent.runCalls[0].tools).toHaveLength(0); + }); + + it("handles server connection failures gracefully", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + mockConnect.mockRejectedValue(new Error("Connection failed")); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + // Should not throw, should continue + const events = await collectEvents(middleware.run(createRunAgentInput(), agent)); + + expect(events.length).toBeGreaterThanOrEqual(2); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("Failed to fetch tools from MCP server"), + expect.any(Error) + ); + + consoleErrorSpy.mockRestore(); + }); + + it("closes client connection after fetching tools", async () => { + mockListTools.mockResolvedValue({ tools: [] }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + await collectEvents(middleware.run(createRunAgentInput(), agent)); + + expect(mockClose).toHaveBeenCalled(); + }); + + it("works with HTTP transport", async () => { + const { StreamableHTTPClientTransport } = await import( + "@modelcontextprotocol/sdk/client/streamableHttp.js" + ); + mockListTools.mockResolvedValue({ tools: [] }); + + const middleware = new MCPAppsMiddleware({ + mcpServers: [{ type: "http", url: "http://localhost:3000" }], + }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + await collectEvents(middleware.run(createRunAgentInput(), agent)); + + expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(new URL("http://localhost:3000")); + }); + + it("works with SSE transport", async () => { + const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js"); + mockListTools.mockResolvedValue({ tools: [] }); + + const middleware = new MCPAppsMiddleware({ + mcpServers: [{ type: "sse", url: "http://localhost:3001/sse" }], + }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + await collectEvents(middleware.run(createRunAgentInput(), agent)); + + expect(SSEClientTransport).toHaveBeenCalledWith(new URL("http://localhost:3001/sse")); + }); + + it("aggregates tools from multiple servers", async () => { + // We need to track which server each call is for + let callCount = 0; + mockListTools.mockImplementation(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + tools: [createMCPToolWithUI("tool-1", "ui://server1/tool1")], + }); + } + return Promise.resolve({ + tools: [createMCPToolWithUI("tool-2", "ui://server2/tool2")], + }); + }); + + const middleware = new MCPAppsMiddleware({ + mcpServers: [httpServerConfig, sseServerConfig], + }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + await collectEvents(middleware.run(createRunAgentInput(), agent)); + + expect(agent.runCalls[0].tools).toHaveLength(2); + expect(agent.runCalls[0].tools.map((t) => t.name)).toContain("tool-1"); + expect(agent.runCalls[0].tools.map((t) => t.name)).toContain("tool-2"); + }); + }); + + // ============================================================================= + // 4. Tool Injection Tests + // ============================================================================= + describe("Tool Injection", () => { + const httpServerConfig: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + + it("merges UI tools with existing input tools", async () => { + mockListTools.mockResolvedValue({ + tools: [createMCPToolWithUI("ui-tool", "ui://server/dashboard")], + }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + const existingTool = createAGUITool("existing-tool"); + const input = createRunAgentInput({ tools: [existingTool] }); + + await collectEvents(middleware.run(input, agent)); + + expect(agent.runCalls[0].tools).toHaveLength(2); + expect(agent.runCalls[0].tools[0].name).toBe("existing-tool"); + expect(agent.runCalls[0].tools[1].name).toBe("ui-tool"); + }); + + it("preserves original input tools", async () => { + mockListTools.mockResolvedValue({ + tools: [createMCPToolWithUI("ui-tool", "ui://server/dashboard")], + }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + const originalTools = [ + createAGUITool("tool-a", "Description A"), + createAGUITool("tool-b", "Description B"), + ]; + const input = createRunAgentInput({ tools: originalTools }); + + await collectEvents(middleware.run(input, agent)); + + const resultTools = agent.runCalls[0].tools; + expect(resultTools[0]).toEqual(originalTools[0]); + expect(resultTools[1]).toEqual(originalTools[1]); + }); + + it("passes enhanced input to next agent", async () => { + mockListTools.mockResolvedValue({ + tools: [createMCPToolWithUI("ui-tool", "ui://server/dashboard")], + }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + const input = createRunAgentInput({ + threadId: "custom-thread", + runId: "custom-run", + state: { key: "value" }, + }); + + await collectEvents(middleware.run(input, agent)); + + expect(agent.runCalls[0].threadId).toBe("custom-thread"); + expect(agent.runCalls[0].runId).toBe("custom-run"); + expect(agent.runCalls[0].state).toEqual({ key: "value" }); + expect(agent.runCalls[0].tools.length).toBe(1); + }); + }); + + // ============================================================================= + // 5. Event Stream Processing Tests + // ============================================================================= + describe("Event Stream Processing", () => { + const httpServerConfig: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + + it("emits non-RUN_FINISHED events immediately", async () => { + mockListTools.mockResolvedValue({ tools: [] }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([ + createRunStartedEvent(), + createTextMessageStartEvent(), + createTextMessageContentEvent(), + createTextMessageEndEvent(), + createRunFinishedEvent(), + ]); + + const receivedEvents: BaseEvent[] = []; + await new Promise((resolve) => { + middleware.run(createRunAgentInput(), agent).subscribe({ + next: (event) => receivedEvents.push(event), + complete: () => resolve(), + }); + }); + + // First event should be RUN_STARTED + expect(receivedEvents[0].type).toBe(EventType.RUN_STARTED); + // Last event should be RUN_FINISHED + expect(receivedEvents[receivedEvents.length - 1].type).toBe(EventType.RUN_FINISHED); + }); + + it("holds back RUN_FINISHED event until stream ends", async () => { + mockListTools.mockResolvedValue({ tools: [] }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new AsyncMockAgent( + [createRunStartedEvent(), createRunFinishedEvent()], + 10 + ); + + const receivedEvents: BaseEvent[] = []; + let finishedReceived = false; + + await new Promise((resolve) => { + middleware.run(createRunAgentInput(), agent).subscribe({ + next: (event) => { + receivedEvents.push(event); + if (event.type === EventType.RUN_FINISHED) { + finishedReceived = true; + } + }, + complete: () => resolve(), + }); + }); + + expect(finishedReceived).toBe(true); + expect(receivedEvents[receivedEvents.length - 1].type).toBe(EventType.RUN_FINISHED); + }); + + it("handles error events correctly", async () => { + mockListTools.mockResolvedValue({ tools: [] }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const testError = new Error("Stream error"); + const agent = new ErrorMockAgent(testError); + + let caughtError: Error | null = null; + await new Promise((resolve) => { + middleware.run(createRunAgentInput(), agent).subscribe({ + error: (err) => { + caughtError = err; + resolve(); + }, + complete: () => resolve(), + }); + }); + + expect(caughtError).toBe(testError); + }); + + it("subscription cleanup works", async () => { + mockListTools.mockResolvedValue({ tools: [] }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new AsyncMockAgent( + [ + createRunStartedEvent(), + createTextMessageStartEvent(), + createTextMessageContentEvent(), + createRunFinishedEvent(), + ], + 50 + ); + + let eventCount = 0; + const subscription = middleware.run(createRunAgentInput(), agent).subscribe({ + next: () => { + eventCount++; + if (eventCount === 2) { + subscription.unsubscribe(); + } + }, + }); + + // Wait a bit to ensure no more events are received after unsubscribe + await new Promise((resolve) => setTimeout(resolve, 200)); + + expect(eventCount).toBe(2); + }); + }); + + // ============================================================================= + // 6. Pending Tool Call Detection Tests + // ============================================================================= + describe("Pending Tool Call Detection", () => { + const httpServerConfig: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + + it("processes pending UI tool calls on stream completion", async () => { + const uiTool = createMCPToolWithUI("ui-weather", "ui://weather/dashboard"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "text", text: "Weather result" }]) + ); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://weather/dashboard", "text/html+mcp", "Dashboard") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + // Create an assistant message with a tool call that won't have a result + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-weather", args: { city: "London" }, id: "tc-1" }, + ]); + + // Agent emits events but doesn't emit a tool result + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + // Set up input with the assistant message containing the tool call + const input = createRunAgentInput({ + messages: [assistantMsg], + }); + + const events = await collectEvents(middleware.run(input, agent)); + + // Should have emitted TOOL_CALL_RESULT and ACTIVITY_SNAPSHOT events + const toolResultEvents = events.filter((e) => e.type === EventType.TOOL_CALL_RESULT); + const activityEvents = events.filter((e) => e.type === EventType.ACTIVITY_SNAPSHOT); + + expect(toolResultEvents.length).toBe(1); + expect(activityEvents.length).toBe(1); + }); + + it("identifies resolved tool calls (role: tool messages)", async () => { + const uiTool = createMCPToolWithUI("ui-weather", "ui://weather/dashboard"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + // Create assistant message with tool call AND a tool result message + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-weather", args: { city: "London" }, id: "tc-1" }, + ]); + const toolResultMsg = createToolResultMessage("tc-1", "Already resolved"); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + const input = createRunAgentInput({ + messages: [assistantMsg, toolResultMsg], + }); + + const events = await collectEvents(middleware.run(input, agent)); + + // Should NOT emit additional TOOL_CALL_RESULT since it's already resolved + const toolResultEvents = events.filter((e) => e.type === EventType.TOOL_CALL_RESULT); + expect(toolResultEvents.length).toBe(0); + }); + + it("handles empty message arrays", async () => { + mockListTools.mockResolvedValue({ + tools: [createMCPToolWithUI("ui-tool", "ui://server/tool")], + }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + const input = createRunAgentInput({ messages: [] }); + const events = await collectEvents(middleware.run(input, agent)); + + // Should complete without errors + expect(events[events.length - 1].type).toBe(EventType.RUN_FINISHED); + }); + + it("handles messages without tool calls", async () => { + mockListTools.mockResolvedValue({ + tools: [createMCPToolWithUI("ui-tool", "ui://server/tool")], + }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + const input = createRunAgentInput({ + messages: [ + { id: "msg-1", role: "user", content: "Hello" }, + { id: "msg-2", role: "assistant", content: "Hi there" }, + ], + }); + + const events = await collectEvents(middleware.run(input, agent)); + + // Should complete without emitting tool results + const toolResultEvents = events.filter((e) => e.type === EventType.TOOL_CALL_RESULT); + expect(toolResultEvents.length).toBe(0); + }); + + it("handles multiple tool calls per message", async () => { + const uiTool1 = createMCPToolWithUI("ui-weather", "ui://weather/dashboard"); + const uiTool2 = createMCPToolWithUI("ui-stocks", "ui://stocks/chart"); + mockListTools.mockResolvedValue({ tools: [uiTool1, uiTool2] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "text", text: "Result" }]) + ); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://test", "text/html+mcp", "") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-weather", args: {}, id: "tc-1" }, + { name: "ui-stocks", args: {}, id: "tc-2" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + const toolResultEvents = events.filter((e) => e.type === EventType.TOOL_CALL_RESULT); + expect(toolResultEvents.length).toBe(2); + }); + }); + + // ============================================================================= + // 7. Tool Execution Tests + // ============================================================================= + describe("Tool Execution", () => { + const httpServerConfig: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + + it("passes correct tool name and arguments", async () => { + const uiTool = createMCPToolWithUI("ui-weather", "ui://weather/dashboard"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "text", text: "Sunny" }]) + ); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://weather/dashboard", "text/html+mcp", "") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-weather", args: { city: "London", units: "metric" }, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + await collectEvents(middleware.run(input, agent)); + + expect(mockCallTool).toHaveBeenCalledWith({ + name: "ui-weather", + arguments: { city: "London", units: "metric" }, + }); + }); + + it("returns raw MCP result", async () => { + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + + const mcpResult = createMCPToolCallResult([ + { type: "text", text: "First" }, + { type: "text", text: "Second" }, + ]); + mockCallTool.mockResolvedValue(mcpResult); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://server/tool", "text/html+mcp", "") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + const activityEvent = events.find((e) => e.type === EventType.ACTIVITY_SNAPSHOT); + expect(activityEvent).toBeDefined(); + expect((activityEvent as any).content.result).toEqual(mcpResult); + }); + + it("handles tool execution errors", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockRejectedValue(new Error("Execution failed")); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + // Should emit error tool result + const toolResultEvents = events.filter((e) => e.type === EventType.TOOL_CALL_RESULT); + expect(toolResultEvents.length).toBe(1); + expect((toolResultEvents[0] as any).content).toContain("error"); + + consoleErrorSpy.mockRestore(); + }); + + it("closes connection after execution", async () => { + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "text", text: "Result" }]) + ); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://server/tool", "text/html+mcp", "") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + await collectEvents(middleware.run(input, agent)); + + // close should be called multiple times (once for listTools, once for callTool, once for readResource) + expect(mockClose).toHaveBeenCalled(); + }); + }); + + // ============================================================================= + // 8. Resource Reading Tests + // ============================================================================= + describe("Resource Reading", () => { + const httpServerConfig: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + + it("reads resource by URI", async () => { + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/dashboard"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "text", text: "Result" }]) + ); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://server/dashboard", "text/html+mcp", "Dashboard") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + await collectEvents(middleware.run(input, agent)); + + expect(mockReadResource).toHaveBeenCalledWith({ + uri: "ui://server/dashboard", + }); + }); + + it("returns first content item", async () => { + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "text", text: "Result" }]) + ); + + const resourceContent = { + uri: "ui://server/tool", + mimeType: "text/html+mcp", + text: "First", + }; + mockReadResource.mockResolvedValue({ + contents: [ + resourceContent, + { uri: "ui://server/tool2", mimeType: "text/html+mcp", text: "Second" }, + ], + }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + const activityEvent = events.find((e) => e.type === EventType.ACTIVITY_SNAPSHOT); + expect((activityEvent as any).content.resource).toEqual(resourceContent); + }); + + it("handles read errors", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "text", text: "Result" }]) + ); + mockReadResource.mockRejectedValue(new Error("Resource not found")); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + // Should emit error result + const toolResultEvents = events.filter((e) => e.type === EventType.TOOL_CALL_RESULT); + expect(toolResultEvents.length).toBe(1); + expect((toolResultEvents[0] as any).content).toContain("error"); + + consoleErrorSpy.mockRestore(); + }); + }); + + // ============================================================================= + // 9. Tool Result Events Tests + // ============================================================================= + describe("Tool Result Events", () => { + const httpServerConfig: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + + it("emits TOOL_CALL_RESULT event with correct toolCallId", async () => { + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "text", text: "Result" }]) + ); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://server/tool", "text/html+mcp", "") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "specific-tc-id" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + const toolResultEvent = events.find((e) => e.type === EventType.TOOL_CALL_RESULT); + expect((toolResultEvent as any).toolCallId).toBe("specific-tc-id"); + }); + + it("extracts text content from MCP result", async () => { + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([ + { type: "text", text: "Line 1" }, + { type: "text", text: "Line 2" }, + ]) + ); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://server/tool", "text/html+mcp", "") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + const toolResultEvent = events.find((e) => e.type === EventType.TOOL_CALL_RESULT); + expect((toolResultEvent as any).content).toBe("Line 1\nLine 2"); + }); + + it("falls back to JSON.stringify for non-text content", async () => { + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "image", data: "base64data" }]) + ); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://server/tool", "text/html+mcp", "") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + const toolResultEvent = events.find((e) => e.type === EventType.TOOL_CALL_RESULT); + expect((toolResultEvent as any).content).toContain("image"); + expect((toolResultEvent as any).content).toContain("base64data"); + }); + + it("emits ACTIVITY_SNAPSHOT with MCP result and resource", async () => { + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + + const mcpResult = createMCPToolCallResult([{ type: "text", text: "Result" }]); + const resourceContent = { + uri: "ui://server/tool", + mimeType: "text/html+mcp", + text: "", + }; + + mockCallTool.mockResolvedValue(mcpResult); + mockReadResource.mockResolvedValue({ contents: [resourceContent] }); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + const activityEvent = events.find((e) => e.type === EventType.ACTIVITY_SNAPSHOT); + expect(activityEvent).toBeDefined(); + expect((activityEvent as any).content.result).toEqual(mcpResult); + expect((activityEvent as any).content.resource).toEqual(resourceContent); + }); + + it("sets activityType to mcp-apps", async () => { + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "text", text: "Result" }]) + ); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://server/tool", "text/html+mcp", "") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + const activityEvent = events.find((e) => e.type === EventType.ACTIVITY_SNAPSHOT); + expect((activityEvent as any).activityType).toBe(MCPAppsActivityType); + expect((activityEvent as any).activityType).toBe("mcp-apps"); + }); + + it("sets replace: true on activity snapshot", async () => { + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "text", text: "Result" }]) + ); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://server/tool", "text/html+mcp", "") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + const activityEvent = events.find((e) => e.type === EventType.ACTIVITY_SNAPSHOT); + expect((activityEvent as any).replace).toBe(true); + }); + }); + + // ============================================================================= + // 10. MCPAppsActivityType Export Tests + // ============================================================================= + describe("MCPAppsActivityType Export", () => { + it("exports MCPAppsActivityType constant", () => { + expect(MCPAppsActivityType).toBeDefined(); + expect(MCPAppsActivityType).toBe("mcp-apps"); + }); + }); + + // ============================================================================= + // 11. Proxied MCP Request Mode Tests + // ============================================================================= + describe("Proxied MCP Request Mode", () => { + it("detects proxied request in forwardedProps", async () => { + const middleware = new MCPAppsMiddleware(); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + const proxiedRequest: ProxiedMCPRequest = { + serverUrl: "http://localhost:3000", + serverType: "http", + method: "ping", + }; + + const input = createRunAgentInput({ + forwardedProps: { __proxiedMCPRequest: proxiedRequest }, + }); + + const events = await collectEvents(middleware.run(input, agent)); + + // Should bypass normal agent flow (agent.run should not be called with our input) + expect(events[0].type).toBe(EventType.RUN_STARTED); + expect(events[events.length - 1].type).toBe(EventType.RUN_FINISHED); + }); + + it("emits RUN_STARTED event", async () => { + mockPing.mockResolvedValue({}); + + const middleware = new MCPAppsMiddleware(); + const agent = new MockAgent([]); + + const proxiedRequest: ProxiedMCPRequest = { + serverUrl: "http://localhost:3000", + serverType: "http", + method: "ping", + }; + + const input = createRunAgentInput({ + runId: "proxy-run", + forwardedProps: { __proxiedMCPRequest: proxiedRequest }, + }); + + const events = await collectEvents(middleware.run(input, agent)); + + expect(events[0].type).toBe(EventType.RUN_STARTED); + expect((events[0] as any).runId).toBe("proxy-run"); + }); + + it("emits RUN_FINISHED with result on success", async () => { + const pingResult = { timestamp: Date.now() }; + mockPing.mockResolvedValue(pingResult); + + const middleware = new MCPAppsMiddleware(); + const agent = new MockAgent([]); + + const proxiedRequest: ProxiedMCPRequest = { + serverUrl: "http://localhost:3000", + serverType: "http", + method: "ping", + }; + + const input = createRunAgentInput({ + forwardedProps: { __proxiedMCPRequest: proxiedRequest }, + }); + + const events = await collectEvents(middleware.run(input, agent)); + + const finishedEvent = events.find((e) => e.type === EventType.RUN_FINISHED); + expect(finishedEvent).toBeDefined(); + expect((finishedEvent as any).result).toEqual(pingResult); + }); + + it("emits RUN_FINISHED with error on failure", async () => { + mockConnect.mockRejectedValue(new Error("Connection refused")); + + const middleware = new MCPAppsMiddleware(); + const agent = new MockAgent([]); + + const proxiedRequest: ProxiedMCPRequest = { + serverUrl: "http://localhost:3000", + serverType: "http", + method: "ping", + }; + + const input = createRunAgentInput({ + forwardedProps: { __proxiedMCPRequest: proxiedRequest }, + }); + + const events = await collectEvents(middleware.run(input, agent)); + + const finishedEvent = events.find((e) => e.type === EventType.RUN_FINISHED); + expect((finishedEvent as any).result.error).toContain("Connection refused"); + }); + + it("bypasses normal agent flow", async () => { + mockPing.mockResolvedValue({}); + + const middleware = new MCPAppsMiddleware({ + mcpServers: [{ type: "http", url: "http://localhost:3001" }], + }); + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + + const proxiedRequest: ProxiedMCPRequest = { + serverUrl: "http://localhost:3000", + serverType: "http", + method: "ping", + }; + + const input = createRunAgentInput({ + forwardedProps: { __proxiedMCPRequest: proxiedRequest }, + }); + + await collectEvents(middleware.run(input, agent)); + + // Agent's run should not have been called + expect(agent.runCalls).toHaveLength(0); + }); + }); +}); diff --git a/middlewares/mcp-apps-middleware/__tests__/test-utils.ts b/middlewares/mcp-apps-middleware/__tests__/test-utils.ts new file mode 100644 index 000000000..c505f4fe8 --- /dev/null +++ b/middlewares/mcp-apps-middleware/__tests__/test-utils.ts @@ -0,0 +1,404 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { vi } from "vitest"; +import { AbstractAgent, BaseEvent, EventType, RunAgentInput, Message, Tool, AssistantMessage } from "@ag-ui/client"; +import { Observable } from "rxjs"; +import { firstValueFrom, toArray } from "rxjs"; + +/** + * Mock MCP Client instance + */ +export interface MockMCPClientInstance { + connect: ReturnType; + close: ReturnType; + listTools: ReturnType; + callTool: ReturnType; + readResource: ReturnType; + notification: ReturnType; + ping: ReturnType; +} + +/** + * Create a mock MCP Client instance + */ +export function createMockMCPClient(): MockMCPClientInstance { + return { + connect: vi.fn().mockResolvedValue(undefined), + close: vi.fn().mockResolvedValue(undefined), + listTools: vi.fn().mockResolvedValue({ tools: [] }), + callTool: vi.fn().mockResolvedValue({ content: [] }), + readResource: vi.fn().mockResolvedValue({ contents: [] }), + notification: vi.fn().mockResolvedValue(undefined), + ping: vi.fn().mockResolvedValue({}), + }; +} + +/** + * Mock MCP tool with UI resource (per SEP-1865) + */ +export interface MockMCPTool { + name: string; + description?: string; + inputSchema?: Record; + _meta?: Record; +} + +/** + * Create an MCP tool with UI resource attached + */ +export function createMCPToolWithUI( + name: string, + resourceUri: string, + description?: string +): MockMCPTool { + return { + name, + description: description || `Tool ${name}`, + inputSchema: { type: "object", properties: {} }, + _meta: { "ui/resourceUri": resourceUri }, + }; +} + +/** + * Create an MCP tool without UI resource + */ +export function createMCPToolWithoutUI(name: string, description?: string): MockMCPTool { + return { + name, + description: description || `Tool ${name}`, + inputSchema: { type: "object", properties: {} }, + }; +} + +/** + * Create an MCP tool with _meta but no ui/resourceUri + */ +export function createMCPToolWithEmptyMeta(name: string): MockMCPTool { + return { + name, + description: `Tool ${name}`, + inputSchema: { type: "object", properties: {} }, + _meta: { someOtherField: "value" }, + }; +} + +/** + * Mock Agent for testing middleware + */ +export class MockAgent extends AbstractAgent { + private events: BaseEvent[]; + public runCalls: RunAgentInput[] = []; + + constructor(events: BaseEvent[] = []) { + super(); + this.events = events; + } + + run(input: RunAgentInput): Observable { + this.runCalls.push(input); + return new Observable((subscriber) => { + for (const event of this.events) { + subscriber.next(event); + } + subscriber.complete(); + }); + } + + setEvents(events: BaseEvent[]): void { + this.events = events; + } +} + +/** + * Mock Agent that emits events asynchronously + */ +export class AsyncMockAgent extends AbstractAgent { + private events: BaseEvent[]; + private delayMs: number; + + constructor(events: BaseEvent[] = [], delayMs: number = 0) { + super(); + this.events = events; + this.delayMs = delayMs; + } + + run(_input: RunAgentInput): Observable { + return new Observable((subscriber) => { + let cancelled = false; + + const emitEvents = async () => { + for (const event of this.events) { + if (cancelled) break; + if (this.delayMs > 0) { + await new Promise((resolve) => setTimeout(resolve, this.delayMs)); + } + if (!cancelled) { + subscriber.next(event); + } + } + if (!cancelled) { + subscriber.complete(); + } + }; + + emitEvents(); + + return () => { + cancelled = true; + }; + }); + } +} + +/** + * Mock Agent that throws an error + */ +export class ErrorMockAgent extends AbstractAgent { + private error: Error; + + constructor(error: Error = new Error("Mock error")) { + super(); + this.error = error; + } + + run(_input: RunAgentInput): Observable { + return new Observable((subscriber) => { + subscriber.error(this.error); + }); + } +} + +/** + * Create a basic RunAgentInput for testing + */ +export function createRunAgentInput(overrides: Partial = {}): RunAgentInput { + return { + threadId: "test-thread", + runId: "test-run", + tools: [], + context: [], + forwardedProps: {}, + state: {}, + messages: [], + ...overrides, + }; +} + +/** + * Create a RUN_STARTED event + */ +export function createRunStartedEvent( + runId: string = "test-run", + threadId: string = "test-thread" +): BaseEvent { + return { + type: EventType.RUN_STARTED, + runId, + threadId, + }; +} + +/** + * Create a RUN_FINISHED event + */ +export function createRunFinishedEvent( + runId: string = "test-run", + threadId: string = "test-thread", + result?: unknown +): BaseEvent { + return { + type: EventType.RUN_FINISHED, + runId, + threadId, + result, + }; +} + +/** + * Create a TEXT_MESSAGE_START event + */ +export function createTextMessageStartEvent(messageId: string = "msg-1"): BaseEvent { + return { + type: EventType.TEXT_MESSAGE_START, + messageId, + role: "assistant", + }; +} + +/** + * Create a TEXT_MESSAGE_CONTENT event + */ +export function createTextMessageContentEvent( + messageId: string = "msg-1", + delta: string = "Hello" +): BaseEvent { + return { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId, + delta, + }; +} + +/** + * Create a TEXT_MESSAGE_END event + */ +export function createTextMessageEndEvent(messageId: string = "msg-1"): BaseEvent { + return { + type: EventType.TEXT_MESSAGE_END, + messageId, + }; +} + +/** + * Create a TOOL_CALL_START event + */ +export function createToolCallStartEvent( + toolCallId: string, + toolCallName: string, + parentMessageId?: string +): BaseEvent { + return { + type: EventType.TOOL_CALL_START, + toolCallId, + toolCallName, + parentMessageId, + }; +} + +/** + * Create a TOOL_CALL_ARGS event + */ +export function createToolCallArgsEvent(toolCallId: string, delta: string): BaseEvent { + return { + type: EventType.TOOL_CALL_ARGS, + toolCallId, + delta, + }; +} + +/** + * Create a TOOL_CALL_END event + */ +export function createToolCallEndEvent(toolCallId: string): BaseEvent { + return { + type: EventType.TOOL_CALL_END, + toolCallId, + }; +} + +/** + * Create a TOOL_CALL_RESULT event + */ +export function createToolCallResultEvent( + toolCallId: string, + content: string, + messageId: string = `result-${toolCallId}` +): BaseEvent { + return { + type: EventType.TOOL_CALL_RESULT, + messageId, + toolCallId, + content, + }; +} + +/** + * Create an assistant message with tool calls + */ +export function createAssistantMessageWithToolCalls( + toolCalls: Array<{ name: string; args?: Record; id?: string }>, + messageId?: string +): AssistantMessage { + return { + id: messageId || `msg-${Math.random().toString(36).substr(2, 9)}`, + role: "assistant", + content: "", + toolCalls: toolCalls.map((tc) => ({ + id: tc.id || `tc-${Math.random().toString(36).substr(2, 9)}`, + type: "function" as const, + function: { + name: tc.name, + arguments: JSON.stringify(tc.args || {}), + }, + })), + }; +} + +/** + * Create a tool result message + */ +export function createToolResultMessage( + toolCallId: string, + content: string, + messageId?: string +): Message { + return { + id: messageId || `msg-${Math.random().toString(36).substr(2, 9)}`, + role: "tool", + toolCallId, + content, + }; +} + +/** + * Create an AG-UI Tool + */ +export function createAGUITool(name: string, description?: string): Tool { + return { + name, + description: description || `Tool ${name}`, + parameters: { type: "object", properties: {} }, + }; +} + +/** + * Collect all events from an Observable + */ +export async function collectEvents(observable: Observable): Promise { + return firstValueFrom(observable.pipe(toArray())); +} + +/** + * Create MCP tool call result (what callTool returns) + */ +export function createMCPToolCallResult( + content: Array<{ type: string; text?: string; [key: string]: unknown }> +): { content: Array<{ type: string; text?: string; [key: string]: unknown }> } { + return { content }; +} + +/** + * Create MCP resource read result + */ +export function createMCPResourceResult( + uri: string, + mimeType: string, + text: string +): { contents: Array<{ uri: string; mimeType: string; text: string }> } { + return { + contents: [{ uri, mimeType, text }], + }; +} + +/** + * Wait for a condition to be true + */ +export async function waitForCondition( + condition: () => boolean, + timeout: number = 1000, + interval: number = 10 +): Promise { + const start = Date.now(); + while (!condition()) { + if (Date.now() - start > timeout) { + throw new Error("Timeout waiting for condition"); + } + await new Promise((resolve) => setTimeout(resolve, interval)); + } +} + +/** + * Generate a random UUID-like string + */ +export function randomId(): string { + return `${Math.random().toString(36).substr(2, 9)}-${Math.random().toString(36).substr(2, 9)}`; +} diff --git a/middlewares/mcp-apps-middleware/package.json b/middlewares/mcp-apps-middleware/package.json new file mode 100644 index 000000000..772e9dee4 --- /dev/null +++ b/middlewares/mcp-apps-middleware/package.json @@ -0,0 +1,37 @@ +{ + "name": "@ag-ui/mcp-apps-middleware", + "author": "Markus Ecker", + "version": "0.0.1", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "clean": "rm -rf dist", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "link:global": "pnpm link --global", + "unlink:global": "pnpm unlink --global" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "peerDependencies": { + "@ag-ui/client": ">=0.0.40", + "rxjs": "7.8.1" + }, + "devDependencies": { + "@ag-ui/client": "workspace:*", + "@types/node": "^20.11.19", + "rxjs": "7.8.1", + "tsup": "^8.0.2", + "typescript": "^5.3.3", + "vitest": "^2.0.0" + } +} diff --git a/middlewares/mcp-apps-middleware/src/index.ts b/middlewares/mcp-apps-middleware/src/index.ts new file mode 100644 index 000000000..d4619975a --- /dev/null +++ b/middlewares/mcp-apps-middleware/src/index.ts @@ -0,0 +1,612 @@ +import { + Middleware, + RunAgentInput, + AbstractAgent, + BaseEvent, + Tool, + EventType, + Message, + ToolCall, + ToolCallResultEvent, + ActivitySnapshotEvent, + RunStartedEvent, + RunFinishedEvent, +} from "@ag-ui/client"; +import { Observable, from, switchMap } from "rxjs"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { randomUUID } from "crypto"; + +/** + * Activity type for MCP Apps events + */ +export const MCPAppsActivityType = "mcp-apps"; + +/** + * Proxied MCP request structure from the frontend iframe + */ +export interface ProxiedMCPRequest { + /** The MCP server URL to connect to */ + serverUrl: string; + /** The MCP server transport type */ + serverType: "http" | "sse"; + /** The JSON-RPC method to call */ + method: string; + /** The JSON-RPC params */ + params?: Record; +} + +/** + * Extract EventWithState type from Middleware.runNextWithState return type + */ +type ExtractObservableType = T extends Observable ? U : never; +type RunNextWithStateReturn = ReturnType; +export type EventWithState = ExtractObservableType; + +/** + * UI Tool with its source server config and resource URI + */ +interface UIToolInfo { + tool: Tool; + serverConfig: MCPClientConfig; + resourceUri: string; +} + +/** + * MCP Client configuration for HTTP transport + */ +export interface MCPClientConfigHTTP { + type: "http"; + url: string; +} + +/** + * MCP Client configuration for SSE transport + */ +export interface MCPClientConfigSSE { + type: "sse"; + url: string; + headers?: Record; +} + +/** + * MCP Client configuration + */ +export type MCPClientConfig = MCPClientConfigHTTP | MCPClientConfigSSE; + +/** + * Configuration for MCPAppsMiddleware + */ +export interface MCPAppsMiddlewareConfig { + /** + * List of MCP server configurations + */ + mcpServers?: MCPClientConfig[]; +} + +/** + * Check if a tool has a UI resource attached (per SEP-1865) + */ +function hasUIResource(tool: { _meta?: Record }): boolean { + return typeof tool._meta?.["ui/resourceUri"] === "string"; +} + +/** + * Extended tool type that includes MCP Apps metadata + */ +export interface MCPAppTool extends Tool { + /** UI resource URI from SEP-1865 */ + uiResourceUri?: string; +} + +/** + * Convert MCP tool to AG-UI tool format, preserving UI resource info + */ +function convertMCPToolToAGUITool(mcpTool: { + name: string; + description?: string; + inputSchema?: Record; + _meta?: Record; +}): Tool { + const tool: Tool = { + name: mcpTool.name, + description: mcpTool.description || "", + parameters: mcpTool.inputSchema || { type: "object", properties: {} }, + }; + + // Store UI resource URI in the description for now + // TODO: Once AG-UI Tool type supports _meta, use that instead + const uiResourceUri = mcpTool._meta?.["ui/resourceUri"]; + if (typeof uiResourceUri === "string") { + tool.description = `${tool.description}\n[UI Resource: ${uiResourceUri}]`; + } + + return tool; +} + +/** + * MCP Apps middleware - fetches UI-enabled tools from MCP servers. + */ +export class MCPAppsMiddleware extends Middleware { + private config: MCPAppsMiddlewareConfig; + /** Map of tool name -> server config for UI tools */ + private uiToolsMap: Map = new Map(); + + constructor(config: MCPAppsMiddlewareConfig = {}) { + super(); + this.config = config; + } + + run(input: RunAgentInput, next: AbstractAgent): Observable { + // Check for proxied MCP request mode + const proxiedRequest = input.forwardedProps + ?.__proxiedMCPRequest as ProxiedMCPRequest | undefined; + if (proxiedRequest) { + return this.handleProxiedMCPRequest(input.runId, proxiedRequest); + } + + // If no MCP servers configured, pass through using runNextWithState + if (!this.config.mcpServers?.length) { + return this.processStream( + this.runNextWithState(input, next), + new Map() + ); + } + + // Fetch UI tools from MCP servers and inject them + return from(this.fetchUITools()).pipe( + switchMap((uiToolInfos) => { + // Build map of tool name -> UIToolInfo + const uiToolsMap = new Map(); + for (const info of uiToolInfos) { + uiToolsMap.set(info.tool.name, info); + } + + // Merge UI tools with existing input tools + const enhancedInput: RunAgentInput = { + ...input, + tools: [...input.tools, ...uiToolInfos.map((info) => info.tool)], + }; + + // Use runNextWithState to get state with each event + return this.processStream( + this.runNextWithState(enhancedInput, next), + uiToolsMap + ); + }) + ); + } + + /** + * Handle a proxied MCP request from the frontend iframe. + * This bypasses the normal agent flow and directly executes the MCP request. + */ + private handleProxiedMCPRequest( + runId: string, + request: ProxiedMCPRequest + ): Observable { + return new Observable((subscriber) => { + // Emit RunStarted + const runStartedEvent: RunStartedEvent = { + type: EventType.RUN_STARTED, + runId, + threadId: runId, + }; + subscriber.next(runStartedEvent); + + // Execute the MCP request + this.executeMCPRequest(request) + .then((result) => { + // Emit RunFinished with the MCP result + const runFinishedEvent: RunFinishedEvent = { + type: EventType.RUN_FINISHED, + runId, + threadId: runId, + result, + }; + subscriber.next(runFinishedEvent); + subscriber.complete(); + }) + .catch((error) => { + // Emit RunFinished with error + const runFinishedEvent: RunFinishedEvent = { + type: EventType.RUN_FINISHED, + runId, + threadId: runId, + result: { error: String(error) }, + }; + subscriber.next(runFinishedEvent); + subscriber.complete(); + }); + }); + } + + /** + * Execute a generic MCP request (tools/call, resources/read, etc.) + */ + private async executeMCPRequest( + request: ProxiedMCPRequest + ): Promise { + let transport; + + if (request.serverType === "sse") { + transport = new SSEClientTransport(new URL(request.serverUrl)); + } else { + transport = new StreamableHTTPClientTransport(new URL(request.serverUrl)); + } + + const client = new Client( + { name: "mcp-apps-middleware", version: "1.0.0" }, + { + capabilities: { + extensions: { + "io.modelcontextprotocol/ui": { + mimeTypes: ["text/html+mcp"], + }, + }, + }, + } + ); + + try { + await client.connect(transport); + + // Per SEP-1865: Forward any method that doesn't start with "ui/" + // Methods starting with "ui/" are handled by the host, not the MCP server + switch (request.method) { + case "tools/call": + return await client.callTool( + request.params as { name: string; arguments?: Record } + ); + case "resources/read": + return await client.readResource(request.params as { uri: string }); + case "notifications/message": + // notifications/message is a one-way notification (no response expected) + await client.notification({ + method: "notifications/message", + params: request.params, + }); + return { success: true }; + case "ping": + return await client.ping(); + default: + throw new Error( + `MCP method not allowed for UI proxy: ${request.method}` + ); + } + } finally { + await client.close(); + } + } + + /** + * Process the event stream, holding back RunFinished events until either: + * a) Another event comes -> flush the held RunFinished immediately + * b) Stream ends -> do special processing, then flush RunFinished and complete + */ + private processStream( + source: Observable, + uiToolsMap: Map + ): Observable { + return new Observable((subscriber) => { + let heldRunFinished: EventWithState | null = null; + let isProcessing = false; + + const subscription = source.subscribe({ + next: (eventWithState) => { + const event = eventWithState.event; + + // If we have a held RunFinished and a new event comes, flush it first + if (heldRunFinished) { + subscriber.next(heldRunFinished.event); + heldRunFinished = null; + } + + // If this is a RunFinished event, hold it back + if (event.type === EventType.RUN_FINISHED) { + heldRunFinished = eventWithState; + } else { + subscriber.next(event); + } + }, + error: (err) => { + // On error, flush any held event and propagate error + if (heldRunFinished) { + subscriber.next(heldRunFinished.event); + heldRunFinished = null; + } + subscriber.error(err); + }, + complete: async () => { + // Stream ended - do special processing if we have a held RunFinished + if (heldRunFinished && !isProcessing) { + isProcessing = true; + + try { + // Find tool calls that don't have a corresponding result message + const pendingToolCalls = this.findPendingToolCalls( + heldRunFinished.messages + ); + + // Filter for UI tool calls (tools we injected from MCP servers) + const pendingUIToolCalls = pendingToolCalls.filter((tc) => + uiToolsMap.has(tc.function.name) + ); + + // Execute pending UI tool calls and emit results + for (const toolCall of pendingUIToolCalls) { + const toolInfo = uiToolsMap.get(toolCall.function.name)!; + try { + const args = JSON.parse(toolCall.function.arguments || "{}"); + const mcpResult = await this.executeToolCall( + toolInfo.serverConfig, + toolCall.function.name, + args + ); + + // Fetch the UI resource + const resource = await this.readResource( + toolInfo.serverConfig, + toolInfo.resourceUri + ); + + // Emit tool result event + const resultEvent: ToolCallResultEvent = { + type: EventType.TOOL_CALL_RESULT, + messageId: randomUUID(), + toolCallId: toolCall.id, + content: this.extractTextContent(mcpResult), + }; + subscriber.next(resultEvent); + + // Emit activity snapshot with full MCP result, resource, and server info + const activityEvent: ActivitySnapshotEvent = { + type: EventType.ACTIVITY_SNAPSHOT, + messageId: randomUUID(), + activityType: MCPAppsActivityType, + content: { + result: mcpResult, + resource, + serverUrl: toolInfo.serverConfig.url, + serverType: toolInfo.serverConfig.type, + toolInput: args, + }, + replace: true, + }; + subscriber.next(activityEvent); + } catch (error) { + console.error( + `Failed to execute UI tool call ${toolCall.function.name}:`, + error + ); + // Emit error result + const errorResult: ToolCallResultEvent = { + type: EventType.TOOL_CALL_RESULT, + messageId: randomUUID(), + toolCallId: toolCall.id, + content: JSON.stringify({ error: String(error) }), + }; + subscriber.next(errorResult); + } + } + + subscriber.next(heldRunFinished.event); + } finally { + heldRunFinished = null; + isProcessing = false; + } + } + subscriber.complete(); + }, + }); + + return () => subscription.unsubscribe(); + }); + } + + /** + * Execute a tool call on the MCP server and return the raw result + */ + private async executeToolCall( + serverConfig: MCPClientConfig, + toolName: string, + args: Record + ): Promise { + let transport; + + if (serverConfig.type === "sse") { + transport = new SSEClientTransport(new URL(serverConfig.url)); + } else { + transport = new StreamableHTTPClientTransport(new URL(serverConfig.url)); + } + + const client = new Client( + { name: "mcp-apps-middleware", version: "1.0.0" }, + { + capabilities: { + extensions: { + "io.modelcontextprotocol/ui": { + mimeTypes: ["text/html+mcp"], + }, + }, + }, + } + ); + + try { + await client.connect(transport); + + const result = await client.callTool({ + name: toolName, + arguments: args, + }); + + return result; + } finally { + await client.close(); + } + } + + /** + * Read a UI resource from the MCP server + */ + private async readResource( + serverConfig: MCPClientConfig, + resourceUri: string + ): Promise { + let transport; + + if (serverConfig.type === "sse") { + transport = new SSEClientTransport(new URL(serverConfig.url)); + } else { + transport = new StreamableHTTPClientTransport(new URL(serverConfig.url)); + } + + const client = new Client( + { name: "mcp-apps-middleware", version: "1.0.0" }, + { + capabilities: { + extensions: { + "io.modelcontextprotocol/ui": { + mimeTypes: ["text/html+mcp"], + }, + }, + }, + } + ); + + try { + await client.connect(transport); + + const result = await client.readResource({ + uri: resourceUri, + }); + + // Return the first content item (the UI resource) + return result.contents[0]; + } finally { + await client.close(); + } + } + + /** + * Extract text content from MCP result, fallback to JSON stringified content + */ + private extractTextContent(mcpResult: unknown): string { + const result = mcpResult as { content?: unknown }; + if (Array.isArray(result.content)) { + const textContent = result.content + .filter( + (c): c is { type: "text"; text: string } => + c && + typeof c === "object" && + c.type === "text" && + typeof c.text === "string" + ) + .map((c) => c.text) + .join("\n"); + return textContent || JSON.stringify(result.content); + } + return JSON.stringify(result.content); + } + + /** + * Find tool calls that don't have a corresponding result (role: "tool") message + */ + private findPendingToolCalls(messages: Message[]): ToolCall[] { + // Collect all tool calls from assistant messages + const allToolCalls: ToolCall[] = []; + for (const message of messages) { + if ( + message.role === "assistant" && + "toolCalls" in message && + message.toolCalls + ) { + allToolCalls.push(...message.toolCalls); + } + } + + // Collect all tool call IDs that have results + const resolvedToolCallIds = new Set(); + for (const message of messages) { + if (message.role === "tool" && "toolCallId" in message) { + resolvedToolCallIds.add(message.toolCallId); + } + } + + // Return tool calls that don't have results + return allToolCalls.filter((tc) => !resolvedToolCallIds.has(tc.id)); + } + + /** + * Connect to all configured MCP servers and fetch tools with UI resources + */ + private async fetchUITools(): Promise { + const allUITools: UIToolInfo[] = []; + + for (const serverConfig of this.config.mcpServers || []) { + try { + const tools = await this.fetchToolsFromServer(serverConfig); + allUITools.push(...tools); + } catch (error) { + console.error( + `Failed to fetch tools from MCP server ${serverConfig.url}:`, + error + ); + } + } + + return allUITools; + } + + /** + * Connect to a single MCP server and fetch its UI-enabled tools + */ + private async fetchToolsFromServer( + serverConfig: MCPClientConfig + ): Promise { + let transport; + + if (serverConfig.type === "sse") { + transport = new SSEClientTransport(new URL(serverConfig.url)); + } else { + transport = new StreamableHTTPClientTransport(new URL(serverConfig.url)); + } + + const client = new Client( + { name: "mcp-apps-middleware", version: "1.0.0" }, + { + capabilities: { + // Advertise MCP Apps UI support per SEP-1865 + extensions: { + "io.modelcontextprotocol/ui": { + mimeTypes: ["text/html+mcp"], + }, + }, + }, + } + ); + + try { + await client.connect(transport); + + // Fetch tools from the server + const response = await client.listTools(); + + // Filter for tools with UI resources and convert to AG-UI format with server config + const uiTools = response.tools + .filter(hasUIResource) + .map((mcpTool) => ({ + tool: convertMCPToolToAGUITool(mcpTool), + serverConfig, + resourceUri: mcpTool._meta!["ui/resourceUri"] as string, + })); + + return uiTools; + } finally { + // Always close the connection + await client.close(); + } + } +} diff --git a/middlewares/mcp-apps-middleware/tsconfig.json b/middlewares/mcp-apps-middleware/tsconfig.json new file mode 100644 index 000000000..ceecfd457 --- /dev/null +++ b/middlewares/mcp-apps-middleware/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "NodeNext", + "lib": ["dom", "dom.iterable", "esnext"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "NodeNext", + "skipLibCheck": true, + "strict": true, + "jsx": "react-jsx", + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "stripInternal": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/middlewares/mcp-apps-middleware/tsup.config.ts b/middlewares/mcp-apps-middleware/tsup.config.ts new file mode 100644 index 000000000..12b69b8fb --- /dev/null +++ b/middlewares/mcp-apps-middleware/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + minify: true, +}); diff --git a/middlewares/mcp-apps-middleware/vitest.config.ts b/middlewares/mcp-apps-middleware/vitest.config.ts new file mode 100644 index 000000000..cf715f4ad --- /dev/null +++ b/middlewares/mcp-apps-middleware/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["__tests__/**/*.test.ts"], + alias: { + "@/": new URL("./src/", import.meta.url).pathname, + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index beb88efcf..a34069b8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,7 +137,7 @@ importers: version: 1.10.6(@types/react@19.2.2)(graphql@16.11.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@copilotkit/runtime': specifier: 1.10.6 - version: 1.10.6(43a54c62826e391639c20a8a0387b983) + version: 1.10.6(iqshwn2xropb3vpfei3flnb2ay) '@copilotkit/runtime-client-gql': specifier: 1.10.6 version: 1.10.6(graphql@16.11.0)(react@19.2.0) @@ -593,7 +593,7 @@ importers: version: 1.2.11(zod@3.25.76) '@copilotkit/runtime': specifier: ^1.10.5 - version: 1.10.6(2d84bc8b2f0c11711d75d851e921b921) + version: 1.10.6(55bgcomojtc3foro7s4axfyssa) '@mastra/client-js': specifier: ^0.15.2 version: 0.15.2(openapi-types@12.1.3)(react@19.2.0)(zod@3.25.76) @@ -827,6 +827,31 @@ importers: specifier: ^5.3.3 version: 5.9.3 + middlewares/mcp-apps-middleware: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.0.0 + version: 1.20.0 + devDependencies: + '@ag-ui/client': + specifier: workspace:* + version: link:../../sdks/typescript/packages/client + '@types/node': + specifier: ^20.11.19 + version: 20.19.21 + rxjs: + specifier: 7.8.1 + version: 7.8.1 + tsup: + specifier: ^8.0.2 + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@20.19.21)(lightningcss@1.30.1) + middlewares/middleware-starter: dependencies: '@ag-ui/client': @@ -1054,8 +1079,8 @@ packages: '@ag-ui/core@0.0.40-alpha.10': resolution: {integrity: sha512-VczUym5UTwVdvJPD95z4cqSEnrygjINqrqZX4ru1gnNlf8PPmhElNPdE/ZiFEgmZAXaxzK6rI+LB6iDx5NltQA==} - '@ag-ui/core@0.0.41': - resolution: {integrity: sha512-yRSh7fweajRGAJxm2IBpC+hfkXleqq0mQOXntMk/UMEgPKWfSRMt7qHL+3mtrGEaeL4fA/rcE41cVSjFDnYSoQ==} + '@ag-ui/core@0.0.42-alpha.3': + resolution: {integrity: sha512-n6/ExW3yct8N+vJYS3YrSah9BKUWi8cEugwJobGr8mOXVVEK8+pHKYLf2UAWsvz6L+mTYoYyn5LD4faOr51DBQ==} '@ag-ui/encoder@0.0.35': resolution: {integrity: sha512-Ym0h0ZKIiD1Ld3+e3v/WQSogY62xs72ysoEBW1kt+dDs79QazBsW5ZlcBBj2DelEs9NrczQLxTVEvrkcvhrHqA==} @@ -1063,8 +1088,8 @@ packages: '@ag-ui/encoder@0.0.40-alpha.10': resolution: {integrity: sha512-aoBhFIcX+SGWzvw/FAK4+mHY6NIz5YA7DchjRCBWAyAGWrdSEObKRgPRifahOrl3hhKgSZo0MYwOin9Q33B+rg==} - '@ag-ui/encoder@0.0.41': - resolution: {integrity: sha512-eg+NUppqC/VQVIEDGD1slNEaRZ7YNVWAhfamntwyPYQnAugcY+Q14XIbROWsZ7YNipfX0cO5yb1WxFrgv1YZrw==} + '@ag-ui/encoder@0.0.42-alpha.3': + resolution: {integrity: sha512-slpJOFlWFNTt3iXV0WiGQjqJBL2SFVuj0m7ddDEEht9SXdA3xlx6x3e3D/on3sVn06nFUTTR5MMlAD8ff1nv0Q==} '@ag-ui/langgraph@0.0.19-alpha.1': resolution: {integrity: sha512-rX8Y4LSxTXWUMFzCspO0c42b6YWGTuciP69Okrh7Lw3kpGsmFq/zmXoBLFz654Yuii2zLHl5mZvkBJ5a3nI6lA==} @@ -1078,8 +1103,8 @@ packages: '@ag-ui/proto@0.0.40-alpha.10': resolution: {integrity: sha512-d7FzAIjWyQzaMEZyMkTMgIyW+qK7LUg2T/MpjAGqWjjcrWGk2Zh6DU/rNMwMbYnK/YlXS3Ljo5a5gI95SrLS+Q==} - '@ag-ui/proto@0.0.41': - resolution: {integrity: sha512-YlVmS8e53EZuMG68WvjNqzxoa/8NYCy3a8yoWsogPf1iZXa1RZ2WbQTi80xGzUnzluwxGSULlg7m7a1/8eXkkA==} + '@ag-ui/proto@0.0.42-alpha.3': + resolution: {integrity: sha512-CKK+SB4FbrZt/lE54Dh/a5lplyFkVwf1YJykazHSb6iKZT65nFARt1Z9tWfR56zNLKX0gJ8/Yq30/fuChcFpZw==} '@ai-sdk/anthropic@2.0.23': resolution: {integrity: sha512-ZEBiiv1UhjGjBwUU63pFhLK5LCSlNDb1idY9K1oZHm5/Fda1cuTojf32tOp0opH0RPbPAN/F8fyyNjbU33n9Kw==} @@ -1698,102 +1723,204 @@ packages: resolution: {integrity: sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==} engines: {node: '>=18.0.0'} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.10': resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.10': resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.10': resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.10': resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.10': resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.10': resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.10': resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.10': resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.10': resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.10': resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.10': resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.10': resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.10': resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.10': resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.10': resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.10': resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.10': resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} engines: {node: '>=18'} @@ -1806,6 +1933,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.10': resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} engines: {node: '>=18'} @@ -1818,6 +1951,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.10': resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} engines: {node: '>=18'} @@ -1830,24 +1969,48 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.10': resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.10': resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.10': resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.10': resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} engines: {node: '>=18'} @@ -5316,6 +5479,35 @@ packages: resolution: {integrity: sha512-JekxQ0RApo4gS4un/iMGsIL1/k4KUBe3HmnGcDvzHuFBdQdudEJgTqcsJC7y6Ul4Yw5CeykgvQbX2XeEJd0+DA==} engines: {node: '>= 20'} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@webcontainer/env@1.1.1': resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} @@ -5488,6 +5680,10 @@ packages: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} engines: {node: '>=8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -5673,6 +5869,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -5713,6 +5913,10 @@ packages: chardet@2.1.0: resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chevrotain-allstar@0.3.1: resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} peerDependencies: @@ -6174,6 +6378,10 @@ packages: babel-plugin-macros: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -6398,6 +6606,11 @@ packages: esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.25.10: resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} engines: {node: '>=18'} @@ -6610,6 +6823,10 @@ packages: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + expect@29.7.0: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7840,7 +8057,6 @@ packages: libsql@0.5.22: resolution: {integrity: sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA==} - cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] lightningcss-darwin-arm64@1.30.1: @@ -7996,6 +8212,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} @@ -8752,9 +8971,16 @@ packages: path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + peek-readable@4.1.0: resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} engines: {node: '>=8'} @@ -9472,6 +9698,9 @@ packages: sift@17.1.3: resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -9538,6 +9767,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} @@ -9549,6 +9781,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stdin-discarder@0.1.0: resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9740,6 +9975,9 @@ packages: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -9750,6 +9988,18 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} @@ -10178,6 +10428,67 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} @@ -10260,6 +10571,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wonka@6.3.5: resolution: {integrity: sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw==} @@ -10434,7 +10750,7 @@ snapshots: rxjs: 7.8.1 zod: 3.25.76 - '@ag-ui/core@0.0.41': + '@ag-ui/core@0.0.42-alpha.3': dependencies: rxjs: 7.8.1 zod: 3.25.76 @@ -10449,10 +10765,10 @@ snapshots: '@ag-ui/core': 0.0.40-alpha.10 '@ag-ui/proto': 0.0.40-alpha.10 - '@ag-ui/encoder@0.0.41': + '@ag-ui/encoder@0.0.42-alpha.3': dependencies: - '@ag-ui/core': 0.0.41 - '@ag-ui/proto': 0.0.41 + '@ag-ui/core': 0.0.42-alpha.3 + '@ag-ui/proto': 0.0.42-alpha.3 '@ag-ui/langgraph@0.0.19-alpha.1(@ag-ui/client@sdks+typescript+packages+client)(@ag-ui/core@sdks+typescript+packages+core)(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: @@ -10481,9 +10797,9 @@ snapshots: '@bufbuild/protobuf': 2.9.0 '@protobuf-ts/protoc': 2.11.1 - '@ag-ui/proto@0.0.41': + '@ag-ui/proto@0.0.42-alpha.3': dependencies: - '@ag-ui/core': 0.0.41 + '@ag-ui/core': 0.0.42-alpha.3 '@bufbuild/protobuf': 2.9.0 '@protobuf-ts/protoc': 2.11.1 @@ -11576,22 +11892,22 @@ snapshots: - encoding - graphql - '@copilotkit/runtime@1.10.6(2d84bc8b2f0c11711d75d851e921b921)': + '@copilotkit/runtime@1.10.6(55bgcomojtc3foro7s4axfyssa)': dependencies: '@ag-ui/client': link:sdks/typescript/packages/client '@ag-ui/core': link:sdks/typescript/packages/core - '@ag-ui/encoder': 0.0.41 + '@ag-ui/encoder': 0.0.42-alpha.3 '@ag-ui/langgraph': 0.0.19-alpha.1(@ag-ui/client@sdks+typescript+packages+client)(@ag-ui/core@sdks+typescript+packages+core)(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@ag-ui/proto': 0.0.41 + '@ag-ui/proto': 0.0.42-alpha.3 '@anthropic-ai/sdk': 0.57.0 '@copilotkit/shared': 1.10.6 '@graphql-yoga/plugin-defer-stream': 3.16.0(graphql-yoga@5.16.0(graphql@16.11.0))(graphql@16.11.0) - '@langchain/aws': 0.1.15(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76))) - '@langchain/community': 0.3.57(8d705aac09841dc81e24dfe2c773558d) + '@langchain/aws': 0.1.15(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))) + '@langchain/community': 0.3.57(75igdgciibrgswysse3hw62tgi) '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) - '@langchain/google-gauth': 0.1.8(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(zod@3.25.76) - '@langchain/langgraph-sdk': 0.0.70(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(react@19.2.0) - '@langchain/openai': 0.4.9(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3) + '@langchain/google-gauth': 0.1.8(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(zod@3.25.76) + '@langchain/langgraph-sdk': 0.0.70(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react@19.2.0) + '@langchain/openai': 0.4.9(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3) '@scarf/scarf': 1.4.0 class-transformer: 0.5.1 class-validator: 0.14.2 @@ -11600,7 +11916,7 @@ snapshots: graphql-scalars: 1.24.2(graphql@16.11.0) graphql-yoga: 5.16.0(graphql@16.11.0) groq-sdk: 0.5.0 - langchain: 0.3.36(@langchain/aws@0.1.15(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76))))(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(axios@1.12.2)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) + langchain: 0.3.36(@langchain/aws@0.1.15(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))))(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(axios@1.12.2)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) openai: 4.104.0(ws@8.18.3)(zod@3.25.76) partial-json: 0.1.7 pino: 9.13.1 @@ -11758,7 +12074,7 @@ snapshots: - ws - youtubei.js - '@copilotkit/runtime@1.10.6(43a54c62826e391639c20a8a0387b983)': + '@copilotkit/runtime@1.10.6(iqshwn2xropb3vpfei3flnb2ay)': dependencies: '@ag-ui/client': link:sdks/typescript/packages/client '@ag-ui/core': link:sdks/typescript/packages/core @@ -11769,7 +12085,7 @@ snapshots: '@copilotkit/shared': 1.10.6 '@graphql-yoga/plugin-defer-stream': 3.16.0(graphql-yoga@5.16.0(graphql@16.11.0))(graphql@16.11.0) '@langchain/aws': 0.1.15(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))) - '@langchain/community': 0.3.57(a6f05470c76b31786172bd3244671918) + '@langchain/community': 0.3.57(37emb7xvj5c4vxjobtfi323cve) '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) '@langchain/google-gauth': 0.1.8(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(zod@3.25.76) '@langchain/langgraph-sdk': 0.0.70(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react@19.2.0) @@ -12057,81 +12373,150 @@ snapshots: '@whatwg-node/promise-helpers': 1.3.2 tslib: 2.8.1 + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.25.10': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.25.10': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.25.10': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.25.10': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.25.10': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.25.10': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.25.10': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.25.10': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.25.10': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.25.10': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.25.10': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.25.10': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.25.10': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.25.10': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.25.10': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.25.10': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.25.10': optional: true '@esbuild/netbsd-arm64@0.25.10': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.25.10': optional: true '@esbuild/openbsd-arm64@0.25.10': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.25.10': optional: true '@esbuild/openharmony-arm64@0.25.10': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.25.10': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.25.10': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.25.10': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.25.10': optional: true @@ -12753,29 +13138,19 @@ snapshots: transitivePeerDependencies: - aws-crt - '@langchain/aws@0.1.15(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))': - dependencies: - '@aws-sdk/client-bedrock-agent-runtime': 3.910.0 - '@aws-sdk/client-bedrock-runtime': 3.910.0 - '@aws-sdk/client-kendra': 3.910.0 - '@aws-sdk/credential-provider-node': 3.910.0 - '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)) - transitivePeerDependencies: - - aws-crt - - '@langchain/community@0.3.57(8d705aac09841dc81e24dfe2c773558d)': + '@langchain/community@0.3.57(37emb7xvj5c4vxjobtfi323cve)': dependencies: - '@browserbasehq/stagehand': 1.14.0(@playwright/test@1.56.0)(deepmerge@4.3.1)(dotenv@16.6.1)(openai@5.12.2(ws@8.18.3)(zod@3.25.76))(zod@3.25.76) + '@browserbasehq/stagehand': 1.14.0(@playwright/test@1.56.0)(deepmerge@4.3.1)(dotenv@16.6.1)(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(zod@3.25.76) '@ibm-cloud/watsonx-ai': 1.7.0 - '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)) - '@langchain/openai': 0.6.16(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3) - '@langchain/weaviate': 0.2.3(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76))) + '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) + '@langchain/openai': 0.6.16(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3) + '@langchain/weaviate': 0.2.3(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))) binary-extensions: 2.3.0 expr-eval: 2.0.2 flat: 5.0.2 ibm-cloud-sdk-core: 5.4.3 js-yaml: 4.1.0 - langchain: 0.3.36(@langchain/aws@0.1.15(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76))))(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(axios@1.12.2)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) + langchain: 0.3.36(@langchain/aws@0.1.15(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))))(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(axios@1.12.2)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) langsmith: 0.3.74(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) openai: 4.104.0(ws@8.18.3)(zod@3.25.76) uuid: 10.0.0 @@ -12820,9 +13195,9 @@ snapshots: - handlebars - peggy - '@langchain/community@0.3.57(a6f05470c76b31786172bd3244671918)': + '@langchain/community@0.3.57(75igdgciibrgswysse3hw62tgi)': dependencies: - '@browserbasehq/stagehand': 1.14.0(@playwright/test@1.56.0)(deepmerge@4.3.1)(dotenv@16.6.1)(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(zod@3.25.76) + '@browserbasehq/stagehand': 1.14.0(@playwright/test@1.56.0)(deepmerge@4.3.1)(dotenv@16.6.1)(openai@5.12.2(ws@8.18.3)(zod@3.25.76))(zod@3.25.76) '@ibm-cloud/watsonx-ai': 1.7.0 '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) '@langchain/openai': 0.6.16(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3) @@ -12925,14 +13300,6 @@ snapshots: transitivePeerDependencies: - zod - '@langchain/google-common@0.1.8(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(zod@3.25.76)': - dependencies: - '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)) - uuid: 10.0.0 - zod-to-json-schema: 3.24.6(zod@3.25.76) - transitivePeerDependencies: - - zod - '@langchain/google-gauth@0.1.8(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(zod@3.25.76)': dependencies: '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) @@ -12943,16 +13310,6 @@ snapshots: - supports-color - zod - '@langchain/google-gauth@0.1.8(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(zod@3.25.76)': - dependencies: - '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)) - '@langchain/google-common': 0.1.8(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(zod@3.25.76) - google-auth-library: 8.9.0 - transitivePeerDependencies: - - encoding - - supports-color - - zod - '@langchain/langgraph-sdk@0.0.70(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react@19.2.0)': dependencies: '@types/json-schema': 7.0.15 @@ -12963,16 +13320,6 @@ snapshots: '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) react: 19.2.0 - '@langchain/langgraph-sdk@0.0.70(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(react@19.2.0)': - dependencies: - '@types/json-schema': 7.0.15 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 9.0.1 - optionalDependencies: - '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)) - react: 19.2.0 - '@langchain/langgraph-sdk@0.1.10(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@types/json-schema': 7.0.15 @@ -12995,17 +13342,6 @@ snapshots: - encoding - ws - '@langchain/openai@0.4.9(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3)': - dependencies: - '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)) - js-tiktoken: 1.0.21 - openai: 4.104.0(ws@8.18.3)(zod@3.25.76) - zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) - transitivePeerDependencies: - - encoding - - ws - '@langchain/openai@0.6.16(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3)': dependencies: '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) @@ -13015,25 +13351,11 @@ snapshots: transitivePeerDependencies: - ws - '@langchain/openai@0.6.16(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3)': - dependencies: - '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)) - js-tiktoken: 1.0.21 - openai: 5.12.2(ws@8.18.3)(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - ws - '@langchain/textsplitters@0.1.0(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))': dependencies: '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) js-tiktoken: 1.0.21 - '@langchain/textsplitters@0.1.0(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))': - dependencies: - '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)) - js-tiktoken: 1.0.21 - '@langchain/weaviate@0.2.3(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))': dependencies: '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) @@ -13042,14 +13364,6 @@ snapshots: transitivePeerDependencies: - encoding - '@langchain/weaviate@0.2.3(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))': - dependencies: - '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)) - uuid: 10.0.0 - weaviate-client: 3.9.0 - transitivePeerDependencies: - - encoding - '@libsql/client@0.15.15': dependencies: '@libsql/core': 0.15.15 @@ -16088,6 +16402,46 @@ snapshots: '@vercel/oidc@3.0.2': {} + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.21)(lightningcss@1.30.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 5.4.21(@types/node@20.19.21)(lightningcss@1.30.1) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.19 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + '@webcontainer/env@1.1.1': {} '@whatwg-node/disposablestack@0.0.6': @@ -16294,6 +16648,8 @@ snapshots: arrify@2.0.1: {} + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} astring@1.9.0: {} @@ -16518,6 +16874,14 @@ snapshots: ccount@2.0.1: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -16545,6 +16909,8 @@ snapshots: chardet@2.1.0: {} + check-error@2.1.1: {} + chevrotain-allstar@0.3.1(chevrotain@11.0.3): dependencies: chevrotain: 11.0.3 @@ -16988,6 +17354,8 @@ snapshots: dedent@1.7.0: {} + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -17253,6 +17621,32 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.10: optionalDependencies: '@esbuild/aix-ppc64': 0.25.10 @@ -17335,7 +17729,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: @@ -17357,7 +17751,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -17583,6 +17977,8 @@ snapshots: exit@0.1.2: {} + expect-type@1.2.2: {} + expect@29.7.0: dependencies: '@jest/expect-utils': 29.7.0 @@ -18345,7 +18741,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.2 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.12.2) + retry-axios: 2.6.0(axios@1.12.2(debug@4.4.3)) tough-cookie: 4.1.4 transitivePeerDependencies: - supports-color @@ -19130,31 +19526,6 @@ snapshots: - openai - ws - langchain@0.3.36(@langchain/aws@0.1.15(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76))))(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(axios@1.12.2)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3): - dependencies: - '@langchain/core': 0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)) - '@langchain/openai': 0.6.16(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3) - '@langchain/textsplitters': 0.1.0(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76))) - js-tiktoken: 1.0.21 - js-yaml: 4.1.0 - jsonpointer: 5.0.1 - langsmith: 0.3.74(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) - openapi-types: 12.1.3 - p-retry: 4.6.2 - uuid: 10.0.0 - yaml: 2.8.1 - zod: 3.25.76 - optionalDependencies: - '@langchain/aws': 0.1.15(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76))) - axios: 1.12.2(debug@4.4.3) - handlebars: 4.7.8 - transitivePeerDependencies: - - '@opentelemetry/api' - - '@opentelemetry/exporter-trace-otlp-proto' - - '@opentelemetry/sdk-trace-base' - - openai - - ws - langium@3.3.1: dependencies: chevrotain: 11.0.3 @@ -19351,6 +19722,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lowlight@1.20.0: dependencies: fault: 1.0.4 @@ -20523,8 +20896,12 @@ snapshots: path-to-regexp@8.3.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@2.0.1: {} + peek-readable@4.1.0: {} pg-cloudflare@1.2.7: @@ -21236,7 +21613,7 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - retry-axios@2.6.0(axios@1.12.2): + retry-axios@2.6.0(axios@1.12.2(debug@4.4.3)): dependencies: axios: 1.12.2(debug@4.4.3) @@ -21544,6 +21921,8 @@ snapshots: sift@17.1.3: {} + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -21594,12 +21973,16 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + state-local@1.0.7: {} statuses@2.0.1: {} statuses@2.0.2: {} + std-env@3.10.0: {} + stdin-discarder@0.1.0: dependencies: bl: 5.1.0 @@ -21828,6 +22211,8 @@ snapshots: throttleit@2.1.0: {} + tinybench@2.9.0: {} + tinyexec@0.3.2: {} tinyexec@1.0.1: {} @@ -21837,6 +22222,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + tippy.js@6.3.7: dependencies: '@popperjs/core': 2.11.8 @@ -22309,6 +22700,69 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vite-node@2.1.9(@types/node@20.19.21)(lightningcss@1.30.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@20.19.21)(lightningcss@1.30.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@20.19.21)(lightningcss@1.30.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.4 + optionalDependencies: + '@types/node': 20.19.21 + fsevents: 2.3.3 + lightningcss: 1.30.1 + + vitest@2.1.9(@types/node@20.19.21)(lightningcss@1.30.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.21)(lightningcss@1.30.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@20.19.21)(lightningcss@1.30.1) + vite-node: 2.1.9(@types/node@20.19.21)(lightningcss@1.30.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.21 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vscode-jsonrpc@8.2.0: {} vscode-languageserver-protocol@3.17.5: @@ -22423,6 +22877,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wonka@6.3.5: {} word-wrap@1.2.5: {} From 22f34b6a217a12137583e3aa55bc9e9c16f86dcb Mon Sep 17 00:00:00 2001 From: Markus Ecker Date: Fri, 5 Dec 2025 13:36:43 +0100 Subject: [PATCH 2/3] use serverId --- .../__tests__/mcp-apps-middleware.test.ts | 117 +++++++++++++++--- middlewares/mcp-apps-middleware/src/index.ts | 71 ++++++++--- 2 files changed, 152 insertions(+), 36 deletions(-) diff --git a/middlewares/mcp-apps-middleware/__tests__/mcp-apps-middleware.test.ts b/middlewares/mcp-apps-middleware/__tests__/mcp-apps-middleware.test.ts index 9a2275b4e..259ba9a62 100644 --- a/middlewares/mcp-apps-middleware/__tests__/mcp-apps-middleware.test.ts +++ b/middlewares/mcp-apps-middleware/__tests__/mcp-apps-middleware.test.ts @@ -6,6 +6,7 @@ import { MCPClientConfig, ProxiedMCPRequest, MCPAppsActivityType, + getServerId, } from "../src/index"; import { MockAgent, @@ -67,10 +68,14 @@ vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ StreamableHTTPClientTransport: vi.fn().mockImplementation(() => ({ type: "http" })), })); -// Mock crypto.randomUUID -vi.mock("crypto", () => ({ - randomUUID: vi.fn(() => `mock-uuid-${Math.random().toString(36).substr(2, 9)}`), -})); +// Mock crypto.randomUUID but keep createHash real +vi.mock("crypto", async () => { + const actual = await vi.importActual("crypto"); + return { + ...actual, + randomUUID: vi.fn(() => `mock-uuid-${Math.random().toString(36).substr(2, 9)}`), + }; +}); describe("MCPAppsMiddleware", () => { beforeEach(() => { @@ -1159,13 +1164,14 @@ describe("MCPAppsMiddleware", () => { // 11. Proxied MCP Request Mode Tests // ============================================================================= describe("Proxied MCP Request Mode", () => { + const httpServerConfig: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + it("detects proxied request in forwardedProps", async () => { - const middleware = new MCPAppsMiddleware(); + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); const proxiedRequest: ProxiedMCPRequest = { - serverUrl: "http://localhost:3000", - serverType: "http", + serverId: getServerId(httpServerConfig), method: "ping", }; @@ -1183,12 +1189,11 @@ describe("MCPAppsMiddleware", () => { it("emits RUN_STARTED event", async () => { mockPing.mockResolvedValue({}); - const middleware = new MCPAppsMiddleware(); + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); const agent = new MockAgent([]); const proxiedRequest: ProxiedMCPRequest = { - serverUrl: "http://localhost:3000", - serverType: "http", + serverId: getServerId(httpServerConfig), method: "ping", }; @@ -1207,12 +1212,11 @@ describe("MCPAppsMiddleware", () => { const pingResult = { timestamp: Date.now() }; mockPing.mockResolvedValue(pingResult); - const middleware = new MCPAppsMiddleware(); + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); const agent = new MockAgent([]); const proxiedRequest: ProxiedMCPRequest = { - serverUrl: "http://localhost:3000", - serverType: "http", + serverId: getServerId(httpServerConfig), method: "ping", }; @@ -1230,12 +1234,11 @@ describe("MCPAppsMiddleware", () => { it("emits RUN_FINISHED with error on failure", async () => { mockConnect.mockRejectedValue(new Error("Connection refused")); - const middleware = new MCPAppsMiddleware(); + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); const agent = new MockAgent([]); const proxiedRequest: ProxiedMCPRequest = { - serverUrl: "http://localhost:3000", - serverType: "http", + serverId: getServerId(httpServerConfig), method: "ping", }; @@ -1249,6 +1252,25 @@ describe("MCPAppsMiddleware", () => { expect((finishedEvent as any).result.error).toContain("Connection refused"); }); + it("emits error for unknown serverId", async () => { + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + const agent = new MockAgent([]); + + const proxiedRequest: ProxiedMCPRequest = { + serverId: "unknown-server-id", + method: "ping", + }; + + const input = createRunAgentInput({ + forwardedProps: { __proxiedMCPRequest: proxiedRequest }, + }); + + const events = await collectEvents(middleware.run(input, agent)); + + const finishedEvent = events.find((e) => e.type === EventType.RUN_FINISHED); + expect((finishedEvent as any).result.error).toContain("Unknown server ID"); + }); + it("bypasses normal agent flow", async () => { mockPing.mockResolvedValue({}); @@ -1258,8 +1280,7 @@ describe("MCPAppsMiddleware", () => { const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); const proxiedRequest: ProxiedMCPRequest = { - serverUrl: "http://localhost:3000", - serverType: "http", + serverId: getServerId({ type: "http", url: "http://localhost:3001" }), method: "ping", }; @@ -1273,4 +1294,64 @@ describe("MCPAppsMiddleware", () => { expect(agent.runCalls).toHaveLength(0); }); }); + + // ============================================================================= + // 12. Server ID Tests + // ============================================================================= + describe("Server ID", () => { + it("generates consistent serverId for same config", () => { + const config: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + const id1 = getServerId(config); + const id2 = getServerId(config); + expect(id1).toBe(id2); + }); + + it("generates different serverIds for different URLs", () => { + const config1: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + const config2: MCPClientConfig = { type: "http", url: "http://localhost:3001" }; + expect(getServerId(config1)).not.toBe(getServerId(config2)); + }); + + it("generates different serverIds for different types", () => { + const config1: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + const config2: MCPClientConfig = { type: "sse", url: "http://localhost:3000" }; + expect(getServerId(config1)).not.toBe(getServerId(config2)); + }); + + it("generates different serverIds for SSE configs with different headers", () => { + const config1: MCPClientConfig = { type: "sse", url: "http://localhost:3000", headers: { Authorization: "token1" } }; + const config2: MCPClientConfig = { type: "sse", url: "http://localhost:3000", headers: { Authorization: "token2" } }; + expect(getServerId(config1)).not.toBe(getServerId(config2)); + }); + + it("includes serverId in ACTIVITY_SNAPSHOT content", async () => { + const httpServerConfig: MCPClientConfig = { type: "http", url: "http://localhost:3000" }; + const uiTool = createMCPToolWithUI("ui-tool", "ui://server/tool"); + mockListTools.mockResolvedValue({ tools: [uiTool] }); + mockCallTool.mockResolvedValue( + createMCPToolCallResult([{ type: "text", text: "Result" }]) + ); + mockReadResource.mockResolvedValue( + createMCPResourceResult("ui://server/tool", "text/html+mcp", "") + ); + + const middleware = new MCPAppsMiddleware({ mcpServers: [httpServerConfig] }); + + const assistantMsg = createAssistantMessageWithToolCalls([ + { name: "ui-tool", args: {}, id: "tc-1" }, + ]); + + const agent = new MockAgent([createRunStartedEvent(), createRunFinishedEvent()]); + const input = createRunAgentInput({ messages: [assistantMsg] }); + + const events = await collectEvents(middleware.run(input, agent)); + + const activityEvent = events.find((e) => e.type === EventType.ACTIVITY_SNAPSHOT); + expect(activityEvent).toBeDefined(); + expect((activityEvent as any).content.serverId).toBe(getServerId(httpServerConfig)); + // Should NOT have serverUrl or serverType + expect((activityEvent as any).content.serverUrl).toBeUndefined(); + expect((activityEvent as any).content.serverType).toBeUndefined(); + }); + }); }); diff --git a/middlewares/mcp-apps-middleware/src/index.ts b/middlewares/mcp-apps-middleware/src/index.ts index d4619975a..8e27db446 100644 --- a/middlewares/mcp-apps-middleware/src/index.ts +++ b/middlewares/mcp-apps-middleware/src/index.ts @@ -16,7 +16,7 @@ import { Observable, from, switchMap } from "rxjs"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { randomUUID } from "crypto"; +import { randomUUID, createHash } from "crypto"; /** * Activity type for MCP Apps events @@ -27,10 +27,8 @@ export const MCPAppsActivityType = "mcp-apps"; * Proxied MCP request structure from the frontend iframe */ export interface ProxiedMCPRequest { - /** The MCP server URL to connect to */ - serverUrl: string; - /** The MCP server transport type */ - serverType: "http" | "sse"; + /** Server identifier (MD5 hash of config) */ + serverId: string; /** The JSON-RPC method to call */ method: string; /** The JSON-RPC params */ @@ -75,6 +73,19 @@ export interface MCPClientConfigSSE { */ export type MCPClientConfig = MCPClientConfigHTTP | MCPClientConfigSSE; +/** + * Generate a stable server ID from config using MD5 hash. + * This allows the frontend to reference servers without knowing their URLs. + */ +export function getServerId(config: MCPClientConfig): string { + const serialized = JSON.stringify({ + type: config.type, + url: config.url, + headers: config.type === "sse" ? (config as MCPClientConfigSSE).headers : undefined, + }); + return createHash("md5").update(serialized).digest("hex"); +} + /** * Configuration for MCPAppsMiddleware */ @@ -132,10 +143,17 @@ export class MCPAppsMiddleware extends Middleware { private config: MCPAppsMiddlewareConfig; /** Map of tool name -> server config for UI tools */ private uiToolsMap: Map = new Map(); + /** Map of serverId -> server config for proxied requests */ + private serverConfigMap: Map = new Map(); constructor(config: MCPAppsMiddlewareConfig = {}) { super(); this.config = config; + // Build server config map for proxied requests + for (const serverConfig of config.mcpServers || []) { + const serverId = getServerId(serverConfig); + this.serverConfigMap.set(serverId, serverConfig); + } } run(input: RunAgentInput, next: AbstractAgent): Observable { @@ -187,6 +205,9 @@ export class MCPAppsMiddleware extends Middleware { request: ProxiedMCPRequest ): Observable { return new Observable((subscriber) => { + // Look up server config by ID + const serverConfig = this.serverConfigMap.get(request.serverId); + // Emit RunStarted const runStartedEvent: RunStartedEvent = { type: EventType.RUN_STARTED, @@ -195,8 +216,21 @@ export class MCPAppsMiddleware extends Middleware { }; subscriber.next(runStartedEvent); + // Handle unknown server ID + if (!serverConfig) { + const runFinishedEvent: RunFinishedEvent = { + type: EventType.RUN_FINISHED, + runId, + threadId: runId, + result: { error: `Unknown server ID: ${request.serverId}` }, + }; + subscriber.next(runFinishedEvent); + subscriber.complete(); + return; + } + // Execute the MCP request - this.executeMCPRequest(request) + this.executeMCPRequest(serverConfig, request.method, request.params) .then((result) => { // Emit RunFinished with the MCP result const runFinishedEvent: RunFinishedEvent = { @@ -226,14 +260,16 @@ export class MCPAppsMiddleware extends Middleware { * Execute a generic MCP request (tools/call, resources/read, etc.) */ private async executeMCPRequest( - request: ProxiedMCPRequest + serverConfig: MCPClientConfig, + method: string, + params?: Record ): Promise { let transport; - if (request.serverType === "sse") { - transport = new SSEClientTransport(new URL(request.serverUrl)); + if (serverConfig.type === "sse") { + transport = new SSEClientTransport(new URL(serverConfig.url)); } else { - transport = new StreamableHTTPClientTransport(new URL(request.serverUrl)); + transport = new StreamableHTTPClientTransport(new URL(serverConfig.url)); } const client = new Client( @@ -254,25 +290,25 @@ export class MCPAppsMiddleware extends Middleware { // Per SEP-1865: Forward any method that doesn't start with "ui/" // Methods starting with "ui/" are handled by the host, not the MCP server - switch (request.method) { + switch (method) { case "tools/call": return await client.callTool( - request.params as { name: string; arguments?: Record } + params as { name: string; arguments?: Record } ); case "resources/read": - return await client.readResource(request.params as { uri: string }); + return await client.readResource(params as { uri: string }); case "notifications/message": // notifications/message is a one-way notification (no response expected) await client.notification({ method: "notifications/message", - params: request.params, + params, }); return { success: true }; case "ping": return await client.ping(); default: throw new Error( - `MCP method not allowed for UI proxy: ${request.method}` + `MCP method not allowed for UI proxy: ${method}` ); } } finally { @@ -360,7 +396,7 @@ export class MCPAppsMiddleware extends Middleware { }; subscriber.next(resultEvent); - // Emit activity snapshot with full MCP result, resource, and server info + // Emit activity snapshot with full MCP result, resource, and server ID const activityEvent: ActivitySnapshotEvent = { type: EventType.ACTIVITY_SNAPSHOT, messageId: randomUUID(), @@ -368,8 +404,7 @@ export class MCPAppsMiddleware extends Middleware { content: { result: mcpResult, resource, - serverUrl: toolInfo.serverConfig.url, - serverType: toolInfo.serverConfig.type, + serverId: getServerId(toolInfo.serverConfig), toolInput: args, }, replace: true, From a4b9cf29abfc11413c580e9c16cbfe1e29522ca5 Mon Sep 17 00:00:00 2001 From: Markus Ecker Date: Fri, 5 Dec 2025 13:39:43 +0100 Subject: [PATCH 3/3] Make package public --- middlewares/mcp-apps-middleware/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/middlewares/mcp-apps-middleware/package.json b/middlewares/mcp-apps-middleware/package.json index 772e9dee4..ff6b56b63 100644 --- a/middlewares/mcp-apps-middleware/package.json +++ b/middlewares/mcp-apps-middleware/package.json @@ -2,6 +2,9 @@ "name": "@ag-ui/mcp-apps-middleware", "author": "Markus Ecker", "version": "0.0.1", + "publishConfig": { + "access": "public" + }, "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts",