From 72cd2af19804769fa9f0c2a080dcf4ca539af55f Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 30 Jan 2026 20:19:35 -0800 Subject: [PATCH 01/12] chore: Upgrade AI SDK from 4.x to 6.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade to AI SDK 6.x to support MCP protocol version 2025-11-25. Package changes: - ai: ^4.3.16 → ^6.0.64 - agents: ^0.2.23 → ^0.3.6 - @ai-sdk/anthropic: ^1.2.12 → ^3.0.33 - @ai-sdk/openai: ^1.3.22 → ^3.0.23 - @ai-sdk/react: ^1.2.12 → ^3.0.66 - @ai-sdk/mcp: (new) ^1.0.16 - @modelcontextprotocol/sdk: ^1.21.0 → ^1.25.3 API migrations: - MCP client imports: "ai" → "@ai-sdk/mcp" - Type rename: LanguageModelV1 → LanguageModel - generateText/streamText: maxSteps → stopWhen(stepCountIs(N)) - tool() API: parameters → inputSchema, execute(params) → execute(input) - Tool call capture: toolCall.args → toolCall.input Co-Authored-By: Claude Opus 4.5 --- packages/mcp-cloudflare/package.json | 5 +- .../mcp-cloudflare/src/server/routes/chat.ts | 6 +- .../src/server/routes/metadata.ts | 2 +- packages/mcp-core/package.json | 11 +- .../src/internal/agents/anthropic-provider.ts | 4 +- .../src/internal/agents/callEmbeddedAgent.ts | 6 +- .../internal/agents/openai-provider.test.ts | 11 +- .../src/internal/agents/openai-provider.ts | 4 +- .../src/internal/agents/tools/utils.ts | 6 +- .../mcp-core/src/internal/agents/types.ts | 4 +- .../mcp-core/src/tools/search-events.test.ts | 14 +- .../src/tools/search-issue-events.test.ts | 14 +- .../mcp-core/src/tools/search-issues.test.ts | 14 +- .../mcp-core/src/tools/use-sentry/handler.ts | 2 +- .../src/tools/use-sentry/tool-wrapper.test.ts | 38 +- packages/mcp-server-evals/package.json | 1 + .../src/evals/utils/toolPredictionScorer.ts | 4 +- packages/mcp-test-client/package.json | 1 + packages/mcp-test-client/src/agent.ts | 4 +- .../src/mcp-test-client-remote.ts | 2 +- .../mcp-test-client/src/mcp-test-client.ts | 4 +- pnpm-lock.yaml | 426 ++++++++++-------- pnpm-workspace.yaml | 13 +- 23 files changed, 330 insertions(+), 266 deletions(-) diff --git a/packages/mcp-cloudflare/package.json b/packages/mcp-cloudflare/package.json index 0e027fa8..404e3e7e 100644 --- a/packages/mcp-cloudflare/package.json +++ b/packages/mcp-cloudflare/package.json @@ -4,9 +4,7 @@ "private": true, "type": "module", "license": "FSL-1.1-ALv2", - "files": [ - "./dist/*" - ], + "files": ["./dist/*"], "exports": { ".": { "types": "./dist/index.ts", @@ -45,6 +43,7 @@ "wrangler": "^4.45.0" }, "dependencies": { + "@ai-sdk/mcp": "catalog:", "@ai-sdk/openai": "catalog:", "@ai-sdk/react": "catalog:", "@cloudflare/workers-oauth-provider": "catalog:", diff --git a/packages/mcp-cloudflare/src/server/routes/chat.ts b/packages/mcp-cloudflare/src/server/routes/chat.ts index edb054a7..7daee4cb 100644 --- a/packages/mcp-cloudflare/src/server/routes/chat.ts +++ b/packages/mcp-cloudflare/src/server/routes/chat.ts @@ -1,7 +1,7 @@ import { Hono, type Context } from "hono"; import { openai } from "@ai-sdk/openai"; -import { streamText, type ToolSet } from "ai"; -import { experimental_createMCPClient } from "ai"; +import { streamText, type ToolSet, stepCountIs } from "ai"; +import { experimental_createMCPClient } from "@ai-sdk/mcp"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import type { Env } from "../types"; import { logInfo, logIssue, logWarn } from "@sentry/mcp-core/telem/logging"; @@ -326,7 +326,7 @@ Start conversations by exploring what's available in their account. Use tools li Remember: You're a test assistant, not a general-purpose helper. Stay focused on testing the MCP integration with their real data.`, maxTokens: 2000, - maxSteps: 10, + stopWhen: stepCountIs(10), experimental_telemetry: { isEnabled: true, }, diff --git a/packages/mcp-cloudflare/src/server/routes/metadata.ts b/packages/mcp-cloudflare/src/server/routes/metadata.ts index edfc85b5..707360a0 100644 --- a/packages/mcp-cloudflare/src/server/routes/metadata.ts +++ b/packages/mcp-cloudflare/src/server/routes/metadata.ts @@ -5,7 +5,7 @@ * without requiring a chat stream to be initialized. */ import { Hono } from "hono"; -import { experimental_createMCPClient } from "ai"; +import { experimental_createMCPClient } from "@ai-sdk/mcp"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import type { Env } from "../types"; import { logIssue, logWarn } from "@sentry/mcp-core/telem/logging"; diff --git a/packages/mcp-core/package.json b/packages/mcp-core/package.json index 06d3cc25..fa478a67 100644 --- a/packages/mcp-core/package.json +++ b/packages/mcp-core/package.json @@ -11,9 +11,7 @@ "author": "Sentry", "description": "Sentry MCP Core - Shared code for MCP transports", "homepage": "https://github.com/getsentry/sentry-mcp", - "keywords": [ - "sentry" - ], + "keywords": ["sentry"], "bugs": { "url": "https://github.com/getsentry/sentry-mcp/issues" }, @@ -21,9 +19,7 @@ "type": "git", "url": "git@github.com:getsentry/sentry-mcp.git" }, - "files": [ - "./dist/*" - ], + "files": ["./dist/*"], "exports": { "./api-client": { "types": "./dist/api-client/index.ts", @@ -144,6 +140,8 @@ "validate-skills": "tsx scripts/validate-skills-mapping.ts" }, "devDependencies": { + "@ai-sdk/provider": "^3.0.6", + "@ai-sdk/provider-utils": "^4.0.11", "@sentry/mcp-server-mocks": "workspace:*", "@sentry/mcp-server-tsconfig": "workspace:*", "msw": "catalog:", @@ -153,6 +151,7 @@ }, "dependencies": { "@ai-sdk/anthropic": "catalog:", + "@ai-sdk/mcp": "catalog:", "@ai-sdk/openai": "catalog:", "@logtape/logtape": "^1.1.1", "@logtape/sentry": "^1.1.1", diff --git a/packages/mcp-core/src/internal/agents/anthropic-provider.ts b/packages/mcp-core/src/internal/agents/anthropic-provider.ts index eee7292d..f2627381 100644 --- a/packages/mcp-core/src/internal/agents/anthropic-provider.ts +++ b/packages/mcp-core/src/internal/agents/anthropic-provider.ts @@ -1,5 +1,5 @@ import { createAnthropic } from "@ai-sdk/anthropic"; -import type { LanguageModelV1 } from "ai"; +import type { LanguageModel } from "ai"; import { USER_AGENT } from "../../version"; // Default configuration constants @@ -24,7 +24,7 @@ export function setAnthropicBaseUrl(baseUrl: string | undefined): void { * - ANTHROPIC_MODEL: Model to use (default: "claude-sonnet-4-5") - env var OK * - Base URL: Must be set via setAnthropicBaseUrl() - NOT from env vars (security risk) */ -export function getAnthropicModel(model?: string): LanguageModelV1 { +export function getAnthropicModel(model?: string): LanguageModel { const defaultModel = process.env.ANTHROPIC_MODEL || DEFAULT_ANTHROPIC_MODEL; const factory = createAnthropic({ diff --git a/packages/mcp-core/src/internal/agents/callEmbeddedAgent.ts b/packages/mcp-core/src/internal/agents/callEmbeddedAgent.ts index 22a1d389..5609f367 100644 --- a/packages/mcp-core/src/internal/agents/callEmbeddedAgent.ts +++ b/packages/mcp-core/src/internal/agents/callEmbeddedAgent.ts @@ -1,4 +1,4 @@ -import { generateText, Output, type Tool, APICallError } from "ai"; +import { generateText, Output, type Tool, APICallError, stepCountIs } from "ai"; import { getAgentProvider } from "./provider-factory"; import { UserInputError, LLMProviderError } from "../../errors"; import type { z } from "zod"; @@ -45,7 +45,7 @@ export async function callEmbeddedAgent< system, prompt, tools, - maxSteps: 5, + stopWhen: stepCountIs(5), // Only include temperature if provider specifies one (e.g., GPT-5 requires temperature=1) ...(provider.getTemperature() !== undefined && { temperature: provider.getTemperature(), @@ -63,7 +63,7 @@ export async function callEmbeddedAgent< for (const toolCall of event.toolCalls) { capturedToolCalls.push({ toolName: toolCall.toolName, - args: toolCall.args, + args: toolCall.input, }); } } diff --git a/packages/mcp-core/src/internal/agents/openai-provider.test.ts b/packages/mcp-core/src/internal/agents/openai-provider.test.ts index 852201e5..6b914bc6 100644 --- a/packages/mcp-core/src/internal/agents/openai-provider.test.ts +++ b/packages/mcp-core/src/internal/agents/openai-provider.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { LanguageModelV3 } from "@ai-sdk/provider"; import { getOpenAIModel, setOpenAIBaseUrl } from "./openai-provider.js"; describe("openai-provider", () => { @@ -21,7 +22,7 @@ describe("openai-provider", () => { describe("base URL configuration", () => { it("uses default base URL when not configured", () => { - const model = getOpenAIModel(); + const model = getOpenAIModel() as LanguageModelV3; expect(model).toBeDefined(); expect(model.modelId).toBe("gpt-5"); @@ -30,7 +31,7 @@ describe("openai-provider", () => { it("uses configured base URL", () => { setOpenAIBaseUrl("https://custom-openai.example.com"); - const model = getOpenAIModel(); + const model = getOpenAIModel() as LanguageModelV3; expect(model).toBeDefined(); expect(model.modelId).toBe("gpt-5"); @@ -39,13 +40,13 @@ describe("openai-provider", () => { describe("model override", () => { it("uses default model when not specified", () => { - const model = getOpenAIModel(); + const model = getOpenAIModel() as LanguageModelV3; expect(model.modelId).toBe("gpt-5"); }); it("uses specified model when provided", () => { - const model = getOpenAIModel("gpt-4"); + const model = getOpenAIModel("gpt-4") as LanguageModelV3; expect(model.modelId).toBe("gpt-4"); }); @@ -53,7 +54,7 @@ describe("openai-provider", () => { it("uses OPENAI_MODEL env var when set", () => { process.env.OPENAI_MODEL = "gpt-4-turbo"; - const model = getOpenAIModel(); + const model = getOpenAIModel() as LanguageModelV3; expect(model.modelId).toBe("gpt-4-turbo"); }); diff --git a/packages/mcp-core/src/internal/agents/openai-provider.ts b/packages/mcp-core/src/internal/agents/openai-provider.ts index b4a2260e..f14e62a2 100644 --- a/packages/mcp-core/src/internal/agents/openai-provider.ts +++ b/packages/mcp-core/src/internal/agents/openai-provider.ts @@ -1,5 +1,5 @@ import { createOpenAI } from "@ai-sdk/openai"; -import type { LanguageModelV1 } from "ai"; +import type { LanguageModel } from "ai"; import { USER_AGENT } from "../../version"; // Default configuration constants @@ -33,7 +33,7 @@ export function setOpenAIBaseUrl(baseUrl: string | undefined): void { * - Vercel AI Gateway: "openai/gpt-4o", "anthropic/claude-sonnet-4.5", etc. * - Other providers: Check their documentation */ -export function getOpenAIModel(model?: string): LanguageModelV1 { +export function getOpenAIModel(model?: string): LanguageModel { const defaultModel = process.env.OPENAI_MODEL || DEFAULT_OPENAI_MODEL; const factory = createOpenAI({ diff --git a/packages/mcp-core/src/internal/agents/tools/utils.ts b/packages/mcp-core/src/internal/agents/tools/utils.ts index dd90fdc4..5b051491 100644 --- a/packages/mcp-core/src/internal/agents/tools/utils.ts +++ b/packages/mcp-core/src/internal/agents/tools/utils.ts @@ -126,12 +126,12 @@ export function agentTool(config: { return tool({ description: config.description, - parameters: config.parameters, + inputSchema: config.parameters, execute: async ( - params: TParameters, + input: TParameters, ): Promise> => { try { - const result = await config.execute(params); + const result = await config.execute(input); return { result }; } catch (error) { return handleAgentToolError(error); diff --git a/packages/mcp-core/src/internal/agents/types.ts b/packages/mcp-core/src/internal/agents/types.ts index df7fa72f..c6166dcb 100644 --- a/packages/mcp-core/src/internal/agents/types.ts +++ b/packages/mcp-core/src/internal/agents/types.ts @@ -1,4 +1,4 @@ -import type { LanguageModelV1, JSONValue } from "ai"; +import type { LanguageModel, JSONValue } from "ai"; /** * Provider options type matching AI SDK's ProviderOptions (LanguageModelV1ProviderMetadata) @@ -19,7 +19,7 @@ export interface EmbeddedAgentProvider { readonly type: AgentProviderType; /** Get a language model instance, optionally with a model override */ - getModel(modelOverride?: string): LanguageModelV1; + getModel(modelOverride?: string): LanguageModel; /** Get provider-specific options for generateText calls */ getProviderOptions(): ProviderOptions; diff --git a/packages/mcp-core/src/tools/search-events.test.ts b/packages/mcp-core/src/tools/search-events.test.ts index 9cbad67f..2650758d 100644 --- a/packages/mcp-core/src/tools/search-events.test.ts +++ b/packages/mcp-core/src/tools/search-events.test.ts @@ -14,11 +14,15 @@ vi.mock("@ai-sdk/openai", () => { }; }); -vi.mock("ai", () => ({ - generateText: vi.fn(), - tool: vi.fn(() => ({ execute: vi.fn() })), - Output: { object: vi.fn(() => ({})) }, -})); +vi.mock("ai", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + generateText: vi.fn(), + tool: vi.fn(() => ({ execute: vi.fn() })), + Output: { object: vi.fn(() => ({})) }, + }; +}); describe("search_events", () => { const mockGenerateText = vi.mocked(generateText); diff --git a/packages/mcp-core/src/tools/search-issue-events.test.ts b/packages/mcp-core/src/tools/search-issue-events.test.ts index 5c2226f9..08452cb4 100644 --- a/packages/mcp-core/src/tools/search-issue-events.test.ts +++ b/packages/mcp-core/src/tools/search-issue-events.test.ts @@ -15,11 +15,15 @@ vi.mock("@ai-sdk/openai", () => { }; }); -vi.mock("ai", () => ({ - generateText: vi.fn(), - tool: vi.fn(() => ({ execute: vi.fn() })), - Output: { object: vi.fn(() => ({})) }, -})); +vi.mock("ai", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + generateText: vi.fn(), + tool: vi.fn(() => ({ execute: vi.fn() })), + Output: { object: vi.fn(() => ({})) }, + }; +}); describe("search_issue_events", () => { const mockGenerateText = vi.mocked(generateText); diff --git a/packages/mcp-core/src/tools/search-issues.test.ts b/packages/mcp-core/src/tools/search-issues.test.ts index f6eddb81..b1271316 100644 --- a/packages/mcp-core/src/tools/search-issues.test.ts +++ b/packages/mcp-core/src/tools/search-issues.test.ts @@ -14,11 +14,15 @@ vi.mock("@ai-sdk/openai", () => { }; }); -vi.mock("ai", () => ({ - generateText: vi.fn(), - tool: vi.fn(() => ({ execute: vi.fn() })), - Output: { object: vi.fn(() => ({})) }, -})); +vi.mock("ai", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + generateText: vi.fn(), + tool: vi.fn(() => ({ execute: vi.fn() })), + Output: { object: vi.fn(() => ({})) }, + }; +}); describe("search_issues", () => { const mockGenerateText = vi.mocked(generateText); diff --git a/packages/mcp-core/src/tools/use-sentry/handler.ts b/packages/mcp-core/src/tools/use-sentry/handler.ts index 4d5b65c9..9e9ef9b6 100644 --- a/packages/mcp-core/src/tools/use-sentry/handler.ts +++ b/packages/mcp-core/src/tools/use-sentry/handler.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; -import { experimental_createMCPClient } from "ai"; +import { experimental_createMCPClient } from "@ai-sdk/mcp"; import { defineTool } from "../../internal/tool-helpers/define"; import type { ServerContext } from "../../types"; import { useSentryAgent } from "./agent"; diff --git a/packages/mcp-core/src/tools/use-sentry/tool-wrapper.test.ts b/packages/mcp-core/src/tools/use-sentry/tool-wrapper.test.ts index 00b6906f..4cf0ef11 100644 --- a/packages/mcp-core/src/tools/use-sentry/tool-wrapper.test.ts +++ b/packages/mcp-core/src/tools/use-sentry/tool-wrapper.test.ts @@ -1,9 +1,16 @@ import { describe, it, expect } from "vitest"; import { z } from "zod"; +import type { ToolExecutionOptions } from "@ai-sdk/provider-utils"; import { wrapToolForAgent } from "./tool-wrapper"; import type { ServerContext } from "../../types"; import type { ToolConfig } from "../types"; +// Helper to create minimal ToolExecutionOptions for testing +const createToolOptions = (): ToolExecutionOptions => ({ + toolCallId: "test-call-id", + messages: [], +}); + // Create a simple mock tool for testing const mockTool: ToolConfig<{ organizationSlug: z.ZodOptional; @@ -46,14 +53,14 @@ describe("wrapToolForAgent", () => { const wrappedTool = wrapToolForAgent(mockTool, { context }); // Call the wrapped tool - // AI SDK tools expect a toolContext parameter (messages, abortSignal, etc.) - const result = await wrappedTool.execute( + // AI SDK 6: execute takes (input, options) directly + const result = (await wrappedTool.execute!( { organizationSlug: "my-org", someParam: "test-value", }, - {} as any, // Empty tool context for testing - ); + createToolOptions(), + )) as { result?: unknown; error?: string }; // Verify the tool was called with correct params expect(result.result).toBeDefined(); @@ -76,12 +83,12 @@ describe("wrapToolForAgent", () => { const wrappedTool = wrapToolForAgent(mockTool, { context }); // Call without providing organizationSlug - const result = await wrappedTool.execute( + const result = (await wrappedTool.execute!( { someParam: "test-value", }, - {} as any, - ); + createToolOptions(), + )) as { result?: unknown; error?: string }; // Verify the constraint was injected expect(result.result).toBeDefined(); @@ -105,12 +112,12 @@ describe("wrapToolForAgent", () => { const wrappedTool = wrapToolForAgent(mockTool, { context }); // Call without providing projectSlug - const result = await wrappedTool.execute( + const result = (await wrappedTool.execute!( { someParam: "test-value", }, - {} as any, - ); + createToolOptions(), + )) as { result?: unknown; error?: string }; // Verify both constraints were injected expect(result.result).toBeDefined(); @@ -134,13 +141,13 @@ describe("wrapToolForAgent", () => { const wrappedTool = wrapToolForAgent(mockTool, { context }); // Provide organizationSlug explicitly (should NOT override since constraint injection doesn't override) - const result = await wrappedTool.execute( + const result = (await wrappedTool.execute!( { organizationSlug: "agent-provided-org", someParam: "test-value", }, - {} as any, - ); + createToolOptions(), + )) as { result?: unknown; error?: string }; expect(result.result).toBeDefined(); const parsed = JSON.parse(result.result as string); @@ -177,7 +184,10 @@ describe("wrapToolForAgent", () => { const wrappedTool = wrapToolForAgent(errorTool, { context }); // Call the tool that throws an error - const result = await wrappedTool.execute({ param: "test" }, {} as any); + const result = (await wrappedTool.execute!( + { param: "test" }, + createToolOptions(), + )) as { result?: unknown; error?: string }; // Verify the error was caught and returned in structured format // Generic errors are wrapped as "System Error" by agentTool for security diff --git a/packages/mcp-server-evals/package.json b/packages/mcp-server-evals/package.json index f81d4105..388f0ec0 100644 --- a/packages/mcp-server-evals/package.json +++ b/packages/mcp-server-evals/package.json @@ -15,6 +15,7 @@ "eval:ci": "vitest run --coverage --reporter=vitest-evals/reporter --reporter=junit --reporter=json --outputFile.json=eval-results.json --outputFile.junit=eval.junit.xml" }, "dependencies": { + "@ai-sdk/mcp": "catalog:", "@ai-sdk/openai": "catalog:", "@modelcontextprotocol/sdk": "catalog:", "@sentry/mcp-core": "workspace:*", diff --git a/packages/mcp-server-evals/src/evals/utils/toolPredictionScorer.ts b/packages/mcp-server-evals/src/evals/utils/toolPredictionScorer.ts index cf45d39e..dcfaf1bb 100644 --- a/packages/mcp-server-evals/src/evals/utils/toolPredictionScorer.ts +++ b/packages/mcp-server-evals/src/evals/utils/toolPredictionScorer.ts @@ -1,8 +1,8 @@ import { openai } from "@ai-sdk/openai"; import { generateObject, type LanguageModel } from "ai"; import { z } from "zod"; -import { experimental_createMCPClient } from "ai"; -import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"; +import { experimental_createMCPClient } from "@ai-sdk/mcp"; +import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio"; // Cache for available tools to avoid reconnecting for each test let cachedTools: string[] | null = null; diff --git a/packages/mcp-test-client/package.json b/packages/mcp-test-client/package.json index be6a4c17..180ac410 100644 --- a/packages/mcp-test-client/package.json +++ b/packages/mcp-test-client/package.json @@ -17,6 +17,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@ai-sdk/mcp": "catalog:", "@ai-sdk/openai": "catalog:", "@modelcontextprotocol/sdk": "catalog:", "@sentry/core": "catalog:", diff --git a/packages/mcp-test-client/src/agent.ts b/packages/mcp-test-client/src/agent.ts index e6b18574..738baafd 100644 --- a/packages/mcp-test-client/src/agent.ts +++ b/packages/mcp-test-client/src/agent.ts @@ -1,5 +1,5 @@ import { openai } from "@ai-sdk/openai"; -import { streamText } from "ai"; +import { streamText, stepCountIs } from "ai"; import { startNewTrace, startSpan } from "@sentry/core"; import type { MCPConnection } from "./types.js"; import { DEFAULT_MODEL } from "./constants.js"; @@ -73,7 +73,7 @@ export async function runAgent( system: SYSTEM_PROMPT, messages: [{ role: "user", content: userPrompt }], tools, - maxSteps, + stopWhen: stepCountIs(maxSteps), experimental_telemetry: { isEnabled: true, }, diff --git a/packages/mcp-test-client/src/mcp-test-client-remote.ts b/packages/mcp-test-client/src/mcp-test-client-remote.ts index 126e5ccd..8ee394a8 100644 --- a/packages/mcp-test-client/src/mcp-test-client-remote.ts +++ b/packages/mcp-test-client/src/mcp-test-client-remote.ts @@ -1,4 +1,4 @@ -import { experimental_createMCPClient } from "ai"; +import { experimental_createMCPClient } from "@ai-sdk/mcp"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { startNewTrace, startSpan } from "@sentry/core"; import { OAuthClient } from "./auth/oauth.js"; diff --git a/packages/mcp-test-client/src/mcp-test-client.ts b/packages/mcp-test-client/src/mcp-test-client.ts index b6edfc88..e7e22019 100644 --- a/packages/mcp-test-client/src/mcp-test-client.ts +++ b/packages/mcp-test-client/src/mcp-test-client.ts @@ -1,5 +1,5 @@ -import { experimental_createMCPClient } from "ai"; -import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"; +import { experimental_createMCPClient } from "@ai-sdk/mcp"; +import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { startNewTrace, startSpan } from "@sentry/core"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df449dd7..b65f46b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,14 +7,17 @@ settings: catalogs: default: '@ai-sdk/anthropic': - specifier: ^1.2.12 - version: 1.2.12 + specifier: ^3.0.33 + version: 3.0.33 + '@ai-sdk/mcp': + specifier: ^1.0.16 + version: 1.0.16 '@ai-sdk/openai': - specifier: ^1.3.22 - version: 1.3.22 + specifier: ^3.0.23 + version: 3.0.23 '@ai-sdk/react': - specifier: ^1.2.12 - version: 1.2.12 + specifier: ^3.0.66 + version: 3.0.66 '@biomejs/biome': specifier: ^1.9.4 version: 1.9.4 @@ -25,8 +28,8 @@ catalogs: specifier: ^4.20251014.0 version: 4.20251014.0 '@modelcontextprotocol/sdk': - specifier: ^1.21.0 - version: 1.22.0 + specifier: ^1.25.3 + version: 1.25.3 '@radix-ui/react-accordion': specifier: ^1.2.11 version: 1.2.11 @@ -70,11 +73,11 @@ catalogs: specifier: ^3.2.4 version: 3.2.4 agents: - specifier: ^0.2.23 - version: 0.2.23 + specifier: ^0.3.6 + version: 0.3.6 ai: - specifier: ^4.3.16 - version: 4.3.16 + specifier: ^6.0.64 + version: 6.0.64 better-sqlite3: specifier: ^11.10.0 version: 11.10.0 @@ -216,18 +219,21 @@ importers: packages/mcp-cloudflare: dependencies: + '@ai-sdk/mcp': + specifier: 'catalog:' + version: 1.0.16(zod@3.25.76) '@ai-sdk/openai': specifier: 'catalog:' - version: 1.3.22(zod@3.25.76) + version: 3.0.23(zod@3.25.76) '@ai-sdk/react': specifier: 'catalog:' - version: 1.2.12(react@19.1.0)(zod@3.25.76) + version: 3.0.66(react@19.1.0)(zod@3.25.76) '@cloudflare/workers-oauth-provider': specifier: 'catalog:' version: 0.0.12 '@modelcontextprotocol/sdk': specifier: 'catalog:' - version: 1.22.0(@cfworker/json-schema@4.1.1) + version: 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) '@radix-ui/react-accordion': specifier: 'catalog:' version: 1.2.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -242,10 +248,10 @@ importers: version: 10.35.0(react@19.1.0) agents: specifier: 'catalog:' - version: 0.2.23(@cloudflare/workers-types@4.20251014.0)(react@19.1.0)(typescript@5.8.3) + version: 0.3.6(@ai-sdk/openai@3.0.23(zod@3.25.76))(@ai-sdk/react@3.0.66(react@19.1.0)(zod@3.25.76))(@cloudflare/ai-chat@0.0.4)(@cloudflare/codemode@0.0.5)(@cloudflare/workers-types@4.20251014.0)(ai@6.0.64(zod@3.25.76))(hono@4.11.4)(react@19.1.0)(zod@3.25.76) ai: specifier: 'catalog:' - version: 4.3.16(react@19.1.0)(zod@3.25.76) + version: 6.0.64(zod@3.25.76) asciinema-player: specifier: ^3.10.0 version: 3.12.1 @@ -290,7 +296,7 @@ importers: version: 1.3.5 workers-mcp: specifier: 'catalog:' - version: 0.1.0-3(@cfworker/json-schema@4.1.1) + version: 0.1.0-3(@cfworker/json-schema@4.1.1)(zod@3.25.76) zod: specifier: 'catalog:' version: 3.25.76 @@ -351,10 +357,13 @@ importers: dependencies: '@ai-sdk/anthropic': specifier: 'catalog:' - version: 1.2.12(zod@3.25.76) + version: 3.0.33(zod@3.25.76) + '@ai-sdk/mcp': + specifier: 'catalog:' + version: 1.0.16(zod@3.25.76) '@ai-sdk/openai': specifier: 'catalog:' - version: 1.3.22(zod@3.25.76) + version: 3.0.23(zod@3.25.76) '@logtape/logtape': specifier: ^1.1.1 version: 1.1.1 @@ -363,13 +372,13 @@ importers: version: 1.1.1(@logtape/logtape@1.1.1) '@modelcontextprotocol/sdk': specifier: 'catalog:' - version: 1.22.0(@cfworker/json-schema@4.1.1) + version: 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) '@sentry/core': specifier: 'catalog:' version: 10.35.0 ai: specifier: 'catalog:' - version: 4.3.16(react@19.1.0)(zod@3.25.76) + version: 6.0.64(zod@3.25.76) dotenv: specifier: 'catalog:' version: 16.6.1 @@ -377,6 +386,12 @@ importers: specifier: 'catalog:' version: 3.25.76 devDependencies: + '@ai-sdk/provider': + specifier: ^3.0.6 + version: 3.0.6 + '@ai-sdk/provider-utils': + specifier: ^4.0.11 + version: 4.0.11(zod@3.25.76) '@sentry/mcp-server-mocks': specifier: workspace:* version: link:../mcp-server-mocks @@ -400,7 +415,7 @@ importers: dependencies: '@modelcontextprotocol/sdk': specifier: 'catalog:' - version: 1.22.0(@cfworker/json-schema@4.1.1) + version: 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) '@sentry/core': specifier: 'catalog:' version: 10.35.0 @@ -441,12 +456,15 @@ importers: packages/mcp-server-evals: dependencies: + '@ai-sdk/mcp': + specifier: 'catalog:' + version: 1.0.16(zod@3.25.76) '@ai-sdk/openai': specifier: 'catalog:' - version: 1.3.22(zod@3.25.76) + version: 3.0.23(zod@3.25.76) '@modelcontextprotocol/sdk': specifier: 'catalog:' - version: 1.22.0(@cfworker/json-schema@4.1.1) + version: 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) '@sentry/mcp-core': specifier: workspace:* version: link:../mcp-core @@ -461,7 +479,7 @@ importers: version: link:../mcp-server-tsconfig ai: specifier: 'catalog:' - version: 4.3.16(react@19.1.0)(zod@3.25.76) + version: 6.0.64(zod@3.25.76) dotenv: specifier: 'catalog:' version: 16.6.1 @@ -498,12 +516,15 @@ importers: packages/mcp-test-client: dependencies: + '@ai-sdk/mcp': + specifier: 'catalog:' + version: 1.0.16(zod@3.25.76) '@ai-sdk/openai': specifier: 'catalog:' - version: 1.3.22(zod@3.25.76) + version: 3.0.23(zod@3.25.76) '@modelcontextprotocol/sdk': specifier: 'catalog:' - version: 1.22.0(@cfworker/json-schema@4.1.1) + version: 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) '@sentry/core': specifier: 'catalog:' version: 10.35.0 @@ -515,7 +536,7 @@ importers: version: 10.35.0 ai: specifier: 'catalog:' - version: 4.3.16(react@19.1.0)(zod@3.25.76) + version: 6.0.64(zod@3.25.76) chalk: specifier: 'catalog:' version: 5.4.1 @@ -553,65 +574,45 @@ importers: packages: - '@ai-sdk/anthropic@1.2.12': - resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - - '@ai-sdk/gateway@2.0.7': - resolution: {integrity: sha512-/AI5AKi4vOK9SEb8Z1dfXkhsJ5NAfWsoJQc96B/mzn2KIrjw5occOjIwD06scuhV9xWlghCoXJT1sQD9QH/tyg==} + '@ai-sdk/anthropic@3.0.33': + resolution: {integrity: sha512-MvQzipOAK99Ct3nX6d8oz2AJ9c8kbiU2uSpGYK3F5oBBa8Gy04+LbYBoB9oH8bbZJxFafkgwBq1+k8xpU7vVxQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai@1.3.22': - resolution: {integrity: sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw==} + '@ai-sdk/gateway@3.0.29': + resolution: {integrity: sha512-zf6yXT+7DcVGWG7ntxVCYC48X/opsWlO5ePvgH8W9DaEVUtkemqKUEzBqowQ778PkZo8sqMnRfD0+fi9HamRRQ==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai@2.0.64': - resolution: {integrity: sha512-+1mqxn42uB32DPZ6kurSyGAmL3MgCaDpkYU7zNDWI4NLy3Zg97RxTsI1jBCGIqkEVvRZKJlIMYtb89OvMnq3AQ==} + '@ai-sdk/mcp@1.0.16': + resolution: {integrity: sha512-4doH7H4o7N4UQZy4+yzGZg+Lzl71wveKvkJE8wux1nOG9BRUghQALo/P3OtyPMLuLgMODVcUzsex5fcRAZr8Yg==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@2.2.8': - resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + '@ai-sdk/openai@3.0.23': + resolution: {integrity: sha512-vFfFadJH+hbrgI4lhC9H/r8qPzuFJFUwZNS8oMI8KujO/woovbE1EWOOGMRGtNVL8PrhhxBfgJzvOKdux3c1gw==} engines: {node: '>=18'} peerDependencies: - zod: ^3.23.8 + zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@3.0.16': - resolution: {integrity: sha512-lsWQY9aDXHitw7C1QRYIbVGmgwyT98TF3MfM8alNIXKpdJdi+W782Rzd9f1RyOfgRmZ08gJ2EYNDhWNK7RqpEA==} + '@ai-sdk/provider-utils@4.0.11': + resolution: {integrity: sha512-y/WOPpcZaBjvNaogy83mBsCRPvbtaK0y1sY9ckRrrbTGMvG2HC/9Y/huqNXKnLAxUIME2PGa2uvF2CDwIsxoXQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider@1.1.3': - resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} - engines: {node: '>=18'} - - '@ai-sdk/provider@2.0.0': - resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} + '@ai-sdk/provider@3.0.6': + resolution: {integrity: sha512-hSfoJtLtpMd7YxKM+iTqlJ0ZB+kJ83WESMiWuWrNVey3X8gg97x0OdAAaeAeclZByCX3UdPOTqhvJdK8qYA3ww==} engines: {node: '>=18'} - '@ai-sdk/react@1.2.12': - resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} - engines: {node: '>=18'} - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.23.8 - peerDependenciesMeta: - zod: - optional: true - - '@ai-sdk/ui-utils@1.2.11': - resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} + '@ai-sdk/react@3.0.66': + resolution: {integrity: sha512-bYxfXaNErVDiUaNlvNXaX+3oKLAeEyHiReJd54i+JTD3HEgRuazHfggzL0MidPnFJmlSZDHRRTGXxhMYh426QA==} engines: {node: '>=18'} peerDependencies: - zod: ^3.23.8 + react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} @@ -797,6 +798,21 @@ packages: '@clack/prompts@0.8.2': resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} + '@cloudflare/ai-chat@0.0.4': + resolution: {integrity: sha512-NGQRt34X/UI+mx9fss7LmTTNBDVlFrtu+7JFoaykLUI738w9gjDplsMbOenCRbSr7UW4ngHGnv3YWUmV99eEnQ==} + peerDependencies: + agents: ^0.3.4 + ai: ^6.0.0 + react: ^19.0.0 + zod: ^3.25.0 || ^4.0.0 + + '@cloudflare/codemode@0.0.5': + resolution: {integrity: sha512-00KNtk0tJBkhJ+DdfDqCqWNDyQ9O3pGOrMZSutXF0MQDCjFIjLO3ZDZ5c4FlMmhsysvcj8TyJYzetaJAY4H+cA==} + peerDependencies: + agents: ^0.3.6 + ai: ^6.0.0 + zod: ^3.25.0 || ^4.0.0 + '@cloudflare/kv-asset-handler@0.4.0': resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} engines: {node: '>=18.0.0'} @@ -1200,6 +1216,12 @@ packages: cpu: [x64] os: [win32] + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@img/sharp-darwin-arm64@0.33.5': resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1379,11 +1401,22 @@ packages: peerDependencies: '@logtape/logtape': ^1.1.1 - '@modelcontextprotocol/sdk@1.22.0': - resolution: {integrity: sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==} + '@modelcontextprotocol/sdk@1.25.2': + resolution: {integrity: sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@modelcontextprotocol/sdk@1.25.3': + resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 peerDependenciesMeta: '@cfworker/json-schema': optional: true @@ -2096,8 +2129,8 @@ packages: '@speed-highlight/core@1.2.7': resolution: {integrity: sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==} - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} '@tailwindcss/node@4.1.11': resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} @@ -2224,9 +2257,6 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/diff-match-patch@1.0.36': - resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} - '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -2304,8 +2334,8 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vercel/oidc@3.0.3': - resolution: {integrity: sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==} + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} engines: {node: '>= 20'} '@vitejs/plugin-react@4.6.0': @@ -2379,31 +2409,31 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} - agents@0.2.23: - resolution: {integrity: sha512-YctplbuIuLocBc/uwFDD+pFk0QMhFf3SZXcl3pkgLYvSL2y1KKC6vpJyWTPu8pqzayspfys9RmEtxzfTtvcDUg==} + agents@0.3.6: + resolution: {integrity: sha512-gJtXDGV2jhPI/WzZOYAM5GJleOq7U2o7fnenE89RJ3Y5klm29O1Pk4sQoLVASO2sTucPNtJRMztQCGL2352B+g==} hasBin: true peerDependencies: - react: '*' + '@ai-sdk/openai': ^3.0.0 + '@ai-sdk/react': ^3.0.0 + '@cloudflare/ai-chat': ^0.0.4 + '@cloudflare/codemode': ^0.0.5 + ai: ^6.0.0 + react: ^19.0.0 viem: '>=2.0.0' x402: ^0.7.1 + zod: ^3.25.0 || ^4.0.0 peerDependenciesMeta: + '@ai-sdk/openai': + optional: true + '@ai-sdk/react': + optional: true viem: optional: true x402: optional: true - ai@4.3.16: - resolution: {integrity: sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==} - engines: {node: '>=18'} - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.23.8 - peerDependenciesMeta: - react: - optional: true - - ai@5.0.89: - resolution: {integrity: sha512-8Nq+ZojGacQrupoJEQLrTDzT5VtR3gyp5AaqFSV3tzsAXlYQ9Igb7QE3yeoEdzOk5IRfDwWL7mDCUD+oBg1hDA==} + ai@6.0.64: + resolution: {integrity: sha512-eH+6FC2THf0rXamPOBAPGNBuMvqv4gE5+IKoWVBrO1TkrjwRiuP3+P6yl7/HcXzN9JLbVe3lL8yaTIfn0ZwvxQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -2716,9 +2746,9 @@ packages: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} - cron-schedule@5.0.4: - resolution: {integrity: sha512-nH0a49E/kSVk6BeFgKZy4uUsy6D2A16p120h5bYD9ILBhQu7o2sJFH+WI4R731TSBQ0dB1Ik7inB/dRAB4C8QQ==} - engines: {node: '>=18'} + cron-schedule@6.0.0: + resolution: {integrity: sha512-BoZaseYGXOo5j5HUwTaegIog3JJbuH4BbrY9A1ArLjXpy+RWb3mV28F/9Gv1dDA7E2L8kngWva4NWisnLTyfgQ==} + engines: {node: '>=20'} cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} @@ -2790,9 +2820,6 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - diff-match-patch@1.0.5: - resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} - diff@8.0.2: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} @@ -3285,6 +3312,9 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} @@ -3331,6 +3361,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -3339,11 +3372,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsondiffpatch@0.6.0: - resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -3877,13 +3905,13 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - partyserver@0.0.75: - resolution: {integrity: sha512-i/18vvdxuGjx+rpQ+fDdExlvQoRb7EfTF+6b+kA2ILEpHemtpLWV8NdgDrOPEklRNdCc/4WlzDtYn05d17aZAQ==} + partyserver@0.1.1: + resolution: {integrity: sha512-g23st+jmE9UgfmDW/EYvUMsyEXqdtRdsOGFkDkWopxYeY6IrW7EbGJ63Nxz8K27kWY4fbr/WWFuY+ocfa/TiDA==} peerDependencies: '@cloudflare/workers-types': ^4.20240729.0 - partysocket@1.1.6: - resolution: {integrity: sha512-LkEk8N9hMDDsDT0iDK0zuwUDFVrVMUXFXCeN3850Ng8wtjPqPBeJlwdeY6ROlJSEh3tPoTTasXoSBYH76y118w==} + partysocket@1.1.10: + resolution: {integrity: sha512-ACfn0P6lQuj8/AqB4L5ZDFcIEbpnIteNNObrlxqV1Ge80GTGhjuJ2sNKwNQlFzhGi4kI7fP/C1Eqh8TR78HjDQ==} path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -4196,9 +4224,6 @@ packages: scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} - secure-json-parse@2.7.0: - resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4915,69 +4940,52 @@ packages: snapshots: - '@ai-sdk/anthropic@1.2.12(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/gateway@2.0.7(zod@3.25.76)': + '@ai-sdk/anthropic@3.0.33(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.16(zod@3.25.76) - '@vercel/oidc': 3.0.3 + '@ai-sdk/provider': 3.0.6 + '@ai-sdk/provider-utils': 4.0.11(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/openai@1.3.22(zod@3.25.76)': + '@ai-sdk/gateway@3.0.29(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/provider': 3.0.6 + '@ai-sdk/provider-utils': 4.0.11(zod@3.25.76) + '@vercel/oidc': 3.1.0 zod: 3.25.76 - '@ai-sdk/openai@2.0.64(zod@3.25.76)': + '@ai-sdk/mcp@1.0.16(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.16(zod@3.25.76) + '@ai-sdk/provider': 3.0.6 + '@ai-sdk/provider-utils': 4.0.11(zod@3.25.76) + pkce-challenge: 5.0.0 zod: 3.25.76 - '@ai-sdk/provider-utils@2.2.8(zod@3.25.76)': + '@ai-sdk/openai@3.0.23(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 1.1.3 - nanoid: 3.3.11 - secure-json-parse: 2.7.0 + '@ai-sdk/provider': 3.0.6 + '@ai-sdk/provider-utils': 4.0.11(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/provider-utils@3.0.16(zod@3.25.76)': + '@ai-sdk/provider-utils@4.0.11(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 2.0.0 - '@standard-schema/spec': 1.0.0 + '@ai-sdk/provider': 3.0.6 + '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 zod: 3.25.76 - '@ai-sdk/provider@1.1.3': + '@ai-sdk/provider@3.0.6': dependencies: json-schema: 0.4.0 - '@ai-sdk/provider@2.0.0': + '@ai-sdk/react@3.0.66(react@19.1.0)(zod@3.25.76)': dependencies: - json-schema: 0.4.0 - - '@ai-sdk/react@1.2.12(react@19.1.0)(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) - '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + '@ai-sdk/provider-utils': 4.0.11(zod@3.25.76) + ai: 6.0.64(zod@3.25.76) react: 19.1.0 swr: 2.3.4(react@19.1.0) throttleit: 2.1.0 - optionalDependencies: - zod: 3.25.76 - - '@ai-sdk/ui-utils@1.2.11(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) - zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) + transitivePeerDependencies: + - zod '@ampproject/remapping@2.3.0': dependencies: @@ -5185,6 +5193,22 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@cloudflare/ai-chat@0.0.4(agents@0.3.6)(ai@6.0.64(zod@3.25.76))(react@19.1.0)(zod@3.25.76)': + dependencies: + agents: 0.3.6(@ai-sdk/openai@3.0.23(zod@3.25.76))(@ai-sdk/react@3.0.66(react@19.1.0)(zod@3.25.76))(@cloudflare/ai-chat@0.0.4)(@cloudflare/codemode@0.0.5)(@cloudflare/workers-types@4.20251014.0)(ai@6.0.64(zod@3.25.76))(hono@4.11.4)(react@19.1.0)(zod@3.25.76) + ai: 6.0.64(zod@3.25.76) + react: 19.1.0 + zod: 3.25.76 + + '@cloudflare/codemode@0.0.5(agents@0.3.6)(ai@6.0.64(zod@3.25.76))(typescript@5.8.3)(zod@3.25.76)': + dependencies: + agents: 0.3.6(@ai-sdk/openai@3.0.23(zod@3.25.76))(@ai-sdk/react@3.0.66(react@19.1.0)(zod@3.25.76))(@cloudflare/ai-chat@0.0.4)(@cloudflare/codemode@0.0.5)(@cloudflare/workers-types@4.20251014.0)(ai@6.0.64(zod@3.25.76))(hono@4.11.4)(react@19.1.0)(zod@3.25.76) + ai: 6.0.64(zod@3.25.76) + zod: 3.25.76 + zod-to-ts: 2.0.0(typescript@5.8.3)(zod@3.25.76) + transitivePeerDependencies: + - typescript + '@cloudflare/kv-asset-handler@0.4.0': dependencies: mime: 3.0.0 @@ -5457,6 +5481,10 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true + '@hono/node-server@1.19.9(hono@4.11.4)': + dependencies: + hono: 4.11.4 + '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.0.4 @@ -5632,8 +5660,33 @@ snapshots: '@logtape/logtape': 1.1.1 '@sentry/core': 9.34.0 - '@modelcontextprotocol/sdk@1.22.0(@cfworker/json-schema@4.1.1)': + '@modelcontextprotocol/sdk@1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.4) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.0 + raw-body: 3.0.1 + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - hono + - supports-color + + '@modelcontextprotocol/sdk@1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76)': dependencies: + '@hono/node-server': 1.19.9(hono@4.11.4) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -5643,6 +5696,8 @@ snapshots: eventsource-parser: 3.0.6 express: 5.1.0 express-rate-limit: 7.5.1(express@5.1.0) + jose: 6.1.3 + json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 raw-body: 3.0.1 zod: 3.25.76 @@ -5650,6 +5705,7 @@ snapshots: optionalDependencies: '@cfworker/json-schema': 4.1.1 transitivePeerDependencies: + - hono - supports-color '@mswjs/interceptors@0.39.2': @@ -6368,7 +6424,7 @@ snapshots: '@speed-highlight/core@1.2.7': {} - '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} '@tailwindcss/node@4.1.11': dependencies: @@ -6491,8 +6547,6 @@ snapshots: '@types/deep-eql@4.0.2': {} - '@types/diff-match-patch@1.0.36': {} - '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -6572,7 +6626,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vercel/oidc@3.0.3': {} + '@vercel/oidc@3.1.0': {} '@vitejs/plugin-react@4.6.0(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: @@ -6678,45 +6732,36 @@ snapshots: transitivePeerDependencies: - supports-color - agents@0.2.23(@cloudflare/workers-types@4.20251014.0)(react@19.1.0)(typescript@5.8.3): + agents@0.3.6(@ai-sdk/openai@3.0.23(zod@3.25.76))(@ai-sdk/react@3.0.66(react@19.1.0)(zod@3.25.76))(@cloudflare/ai-chat@0.0.4)(@cloudflare/codemode@0.0.5)(@cloudflare/workers-types@4.20251014.0)(ai@6.0.64(zod@3.25.76))(hono@4.11.4)(react@19.1.0)(zod@3.25.76): dependencies: - '@ai-sdk/openai': 2.0.64(zod@3.25.76) '@cfworker/json-schema': 4.1.1 - '@modelcontextprotocol/sdk': 1.22.0(@cfworker/json-schema@4.1.1) - ai: 5.0.89(zod@3.25.76) - cron-schedule: 5.0.4 + '@cloudflare/ai-chat': 0.0.4(agents@0.3.6)(ai@6.0.64(zod@3.25.76))(react@19.1.0)(zod@3.25.76) + '@cloudflare/codemode': 0.0.5(agents@0.3.6)(ai@6.0.64(zod@3.25.76))(typescript@5.8.3)(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) + ai: 6.0.64(zod@3.25.76) + cron-schedule: 6.0.0 json-schema: 0.4.0 json-schema-to-typescript: 15.0.4 mimetext: 3.0.27 nanoid: 5.1.6 - partyserver: 0.0.75(@cloudflare/workers-types@4.20251014.0) - partysocket: 1.1.6 + partyserver: 0.1.1(@cloudflare/workers-types@4.20251014.0) + partysocket: 1.1.10 react: 19.1.0 yargs: 18.0.0 zod: 3.25.76 - zod-to-ts: 2.0.0(typescript@5.8.3)(zod@3.25.76) + optionalDependencies: + '@ai-sdk/openai': 3.0.23(zod@3.25.76) + '@ai-sdk/react': 3.0.66(react@19.1.0)(zod@3.25.76) transitivePeerDependencies: - '@cloudflare/workers-types' + - hono - supports-color - - typescript - - ai@4.3.16(react@19.1.0)(zod@3.25.76): - dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) - '@ai-sdk/react': 1.2.12(react@19.1.0)(zod@3.25.76) - '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) - '@opentelemetry/api': 1.9.0 - jsondiffpatch: 0.6.0 - zod: 3.25.76 - optionalDependencies: - react: 19.1.0 - ai@5.0.89(zod@3.25.76): + ai@6.0.64(zod@3.25.76): dependencies: - '@ai-sdk/gateway': 2.0.7(zod@3.25.76) - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.16(zod@3.25.76) + '@ai-sdk/gateway': 3.0.29(zod@3.25.76) + '@ai-sdk/provider': 3.0.6 + '@ai-sdk/provider-utils': 4.0.11(zod@3.25.76) '@opentelemetry/api': 1.9.0 zod: 3.25.76 @@ -7014,7 +7059,7 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cron-schedule@5.0.4: {} + cron-schedule@6.0.0: {} cross-spawn@7.0.6: dependencies: @@ -7065,8 +7110,6 @@ snapshots: dependencies: dequal: 2.0.3 - diff-match-patch@1.0.5: {} - diff@8.0.2: {} dotenv-cli@8.0.0: @@ -7580,6 +7623,8 @@ snapshots: jiti@2.4.2: {} + jose@6.1.3: {} + js-base64@3.7.8: {} js-tokens@4.0.0: {} @@ -7640,16 +7685,12 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} json5@2.2.3: {} - jsondiffpatch@0.6.0: - dependencies: - '@types/diff-match-patch': 1.0.36 - chalk: 5.4.1 - diff-match-patch: 1.0.5 - jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -8400,12 +8441,12 @@ snapshots: parseurl@1.3.3: {} - partyserver@0.0.75(@cloudflare/workers-types@4.20251014.0): + partyserver@0.1.1(@cloudflare/workers-types@4.20251014.0): dependencies: '@cloudflare/workers-types': 4.20251014.0 nanoid: 5.1.6 - partysocket@1.1.6: + partysocket@1.1.10: dependencies: event-target-polyfill: 0.0.4 @@ -8772,8 +8813,6 @@ snapshots: scheduler@0.26.0: {} - secure-json-parse@2.7.0: {} - semver@6.3.1: {} semver@7.7.2: {} @@ -9478,10 +9517,10 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20251011.0 '@cloudflare/workerd-windows-64': 1.20251011.0 - workers-mcp@0.1.0-3(@cfworker/json-schema@4.1.1): + workers-mcp@0.1.0-3(@cfworker/json-schema@4.1.1)(zod@3.25.76): dependencies: '@clack/prompts': 0.8.2 - '@modelcontextprotocol/sdk': 1.22.0(@cfworker/json-schema@4.1.1) + '@modelcontextprotocol/sdk': 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.4.1 fs-extra: 11.3.0 @@ -9497,6 +9536,7 @@ snapshots: - '@75lb/nature' - '@cfworker/json-schema' - supports-color + - zod wrangler@4.45.0(@cloudflare/workers-types@4.20251014.0): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 40107daf..4b684246 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,15 +2,16 @@ packages: - packages/* catalog: - "@ai-sdk/anthropic": ^1.2.12 - "@ai-sdk/openai": ^1.3.22 - "@ai-sdk/react": ^1.2.12 + "@ai-sdk/anthropic": ^3.0.33 + "@ai-sdk/mcp": ^1.0.16 + "@ai-sdk/openai": ^3.0.23 + "@ai-sdk/react": ^3.0.66 "@biomejs/biome": ^1.9.4 "@cloudflare/vite-plugin": ^1.13.15 "@cloudflare/vitest-pool-workers": ^0.8.47 "@cloudflare/workers-oauth-provider": ^0.0.12 "@cloudflare/workers-types": ^4.20251014.0 - "@modelcontextprotocol/sdk": ^1.21.0 + "@modelcontextprotocol/sdk": ^1.25.3 "@radix-ui/react-accordion": ^1.2.11 "@radix-ui/react-slot": ^1.2.3 "@sentry/cloudflare": 10.35.0 @@ -25,8 +26,8 @@ catalog: "@types/react-dom": ^19.1.6 "@vitejs/plugin-react": ^4.6.0 "@vitest/coverage-v8": ^3.2.4 - agents: ^0.2.23 - ai: ^4.3.16 + agents: ^0.3.6 + ai: ^6.0.64 better-sqlite3: ^11.10.0 chalk: ^5.4.1 class-variance-authority: ^0.7.1 From 0eb7f98c90eae4c14f9ecb43eb114449c54af203 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 30 Jan 2026 21:09:05 -0800 Subject: [PATCH 02/12] fix(test-client): Complete AI SDK 6.x migration for toolCall.input The AI SDK 6.x migration renamed toolCall.args to toolCall.input. This was correctly updated in callEmbeddedAgent.ts but missed in the test client's agent.ts, causing logTool to receive undefined instead of the actual tool arguments. Co-Authored-By: Claude Opus 4.5 --- packages/mcp-test-client/src/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-test-client/src/agent.ts b/packages/mcp-test-client/src/agent.ts index 738baafd..de0b22d8 100644 --- a/packages/mcp-test-client/src/agent.ts +++ b/packages/mcp-test-client/src/agent.ts @@ -90,7 +90,7 @@ export async function runAgent( const toolCall = toolCalls[i]; const toolResult = toolResults?.[i]; - logTool(toolCall.toolName, toolCall.args); + logTool(toolCall.toolName, toolCall.input); // Show the actual tool result if available if (toolResult?.result) { From 22b36d10c3d0b8c703f3dae657ac6172a7ba42a7 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 30 Jan 2026 21:19:56 -0800 Subject: [PATCH 03/12] fix(cloudflare): Complete AI SDK 6.x migration for chat components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate the cloudflare chat UI to be compatible with AI SDK 5+/6.x: **Client-side changes:** - Replace `Message` type with `UIMessage` (requires `parts` array) - Migrate `useChat` to new API: - Use `DefaultChatTransport` with `api` option - Manage input state manually (removed from hook) - `append` → `sendMessage` - `reload` → `regenerate` - `handleInputChange`/`handleSubmit` → manual handlers - Update all components to use `UIMessage` type - Import from `@ai-sdk/react` instead of `ai/react` **Server-side changes:** - `maxTokens` → `maxOutputTokens` - `toDataStreamResponse` → `toUIMessageStreamResponse` **Dependency fixes:** - Add pnpm override for `@modelcontextprotocol/sdk: ^1.25.3` to fix version mismatch between direct dep (1.25.3) and `agents` package (1.25.2) which caused TypeScript type conflicts Co-Authored-By: Claude Opus 4.5 --- package.json | 5 +- .../client/components/chat/chat-messages.tsx | 45 +++--- .../src/client/components/chat/chat-ui.tsx | 4 +- .../src/client/components/chat/chat.tsx | 150 +++++++++++------- .../src/client/components/chat/types.ts | 16 +- .../src/client/hooks/use-persisted-chat.ts | 67 ++++---- .../mcp-cloudflare/src/server/routes/chat.ts | 4 +- pnpm-lock.yaml | 52 ++---- 8 files changed, 174 insertions(+), 169 deletions(-) diff --git a/package.json b/package.json index 97493e32..0a834841 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,10 @@ "sharp", "simple-git-hooks", "workerd" - ] + ], + "overrides": { + "@modelcontextprotocol/sdk": "^1.25.3" + } }, "devDependencies": { "@types/json-schema": "^7.0.15" diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat-messages.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat-messages.tsx index fadf5672..46c06fe2 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat-messages.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat-messages.tsx @@ -3,15 +3,18 @@ import { Loader2, AlertCircle } from "lucide-react"; import { Button } from "../ui/button"; import { MessagePart } from "."; import { ToolActions } from "../ui/tool-actions"; -import type { Message, ProcessedMessagePart, ChatMessagesProps } from "./types"; +import type { ProcessedMessagePart, ChatMessagesProps } from "./types"; import { isAuthError, getErrorMessage } from "../../utils/chat-error-handler"; import { useAuth } from "../../contexts/auth-context"; +// Import UIMessage from our types (re-exported as Message for compatibility) +import type { UIMessage } from "@ai-sdk/react"; + // Cache for stable part objects to avoid recreating them -const partCache = new WeakMap(); +const partCache = new WeakMap(); function processMessages( - messages: Message[], + messages: UIMessage[], isChatLoading: boolean, isLocalStreaming?: boolean, isMessageStreaming?: (messageId: string) => boolean, @@ -28,23 +31,25 @@ function processMessages( if (message.parts && message.parts.length > 0) { const lastPartIndex = message.parts.length - 1; - message.parts.forEach((part, partIndex) => { - const isLastPartOfLastMessage = - isLastMessage && partIndex === lastPartIndex; - - allParts.push({ - part, - messageId: message.id, - messageRole: message.role, - partIndex, - // Stream if it's AI response OR local streaming simulation - isStreaming: - (isLastPartOfLastMessage && - isChatLoading && - part.type === "text") || - (part.type === "text" && !!isMessageStreaming?.(message.id)), - }); - }); + message.parts.forEach( + (part: UIMessage["parts"][number], partIndex: number) => { + const isLastPartOfLastMessage = + isLastMessage && partIndex === lastPartIndex; + + allParts.push({ + part, + messageId: message.id, + messageRole: message.role, + partIndex, + // Stream if it's AI response OR local streaming simulation + isStreaming: + (isLastPartOfLastMessage && + isChatLoading && + part.type === "text") || + (part.type === "text" && !!isMessageStreaming?.(message.id)), + }); + }, + ); } else if (message.content) { // Use cached part object to maintain stable references let part = partCache.get(message); diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat-ui.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat-ui.tsx index 520a27f1..8993a12a 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat-ui.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat-ui.tsx @@ -6,7 +6,7 @@ import ScrollToBottom from "react-scroll-to-bottom"; import { Button } from "../ui/button"; import { ChatInput, ChatMessages } from "."; -import type { Message } from "ai/react"; +import type { UIMessage } from "@ai-sdk/react"; // Constant empty function to avoid creating new instances on every render const EMPTY_FUNCTION = () => {}; @@ -28,7 +28,7 @@ const SAMPLE_PROMPTS = [ ] as const; interface ChatUIProps { - messages: Message[]; + messages: UIMessage[]; input: string; error?: Error | null; isChatLoading: boolean; diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat.tsx index 12d4b1bd..45284392 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat.tsx @@ -1,7 +1,8 @@ "use client"; import { useChat } from "@ai-sdk/react"; -import { useEffect, useRef, useCallback } from "react"; +import { DefaultChatTransport, type UIMessage } from "ai"; +import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { AuthForm, ChatUI } from "."; import { useAuth } from "../../contexts/auth-context"; import { Bot, Loader2, LogOut, PanelLeftOpen, Sparkles } from "lucide-react"; @@ -42,32 +43,54 @@ export function Chat({ isOpen, onClose, onLogout }: ChatProps) { isMessageStreaming, } = useStreamingSimulation(); + // Manage input state manually (AI SDK 5+ removed built-in input management) + const [input, setInput] = useState(""); + + // Create transport with endpoint mode in body - memoize to avoid recreation + const transport = useMemo( + () => + new DefaultChatTransport({ + api: "/api/chat", + body: { endpointMode }, + }), + [endpointMode], + ); + const { messages, - input, - handleInputChange, - handleSubmit, + sendMessage, status, stop, error, - reload, + regenerate, setMessages, - setInput, - append, } = useChat({ - api: "/api/chat", // No auth header needed - server reads from cookie - // No ID to disable useChat's built-in persistence // We handle persistence manually via usePersistedChat hook - initialMessages, - // Enable sending the data field with messages for custom message types - sendExtraMessageFields: true, - // Pass endpoint mode to the API - body: { - endpointMode, - }, + messages: initialMessages, + transport, }); + // Handle input change (manually managed in AI SDK 5+) + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { + setInput(e.target.value); + }, + [], + ); + + // Handle form submission (manually managed in AI SDK 5+) + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim()) return; + + sendMessage({ text: input }); + setInput(""); + }, + [input, sendMessage], + ); + // No need for custom scroll handling - react-scroll-to-bottom handles it // Clear messages function - used locally for /clear command and logout @@ -215,7 +238,7 @@ Try asking me things like: ) { hadAuthErrorRef.current = false; // Retry the failed message - reload(); + regenerate(); } // Reset retry state on successful completion (no error) @@ -225,7 +248,24 @@ Try asking me things like: // Update auth state ref wasAuthenticatedRef.current = isAuthenticated; - }, [isAuthenticated, error, reload]); + }, [isAuthenticated, error, regenerate]); + + // Helper to create a UIMessage with parts (AI SDK 5+ format) + const createUIMessage = useCallback( + ( + id: string, + role: "user" | "assistant" | "system", + text: string, + data?: Record, + ): UIMessage => ({ + id, + role, + parts: [{ type: "text", text }], + createdAt: new Date(), + ...(data && { data }), + }), + [], + ); // Handle slash commands const handleSlashCommand = useCallback( @@ -234,34 +274,32 @@ Try asking me things like: setInput(""); // Add the slash command as a user message first - const userMessage = { - id: Date.now().toString(), - role: "user" as const, - content: `/${command}`, - createdAt: new Date(), - }; + const userMessage = createUIMessage( + Date.now().toString(), + "user", + `/${command}`, + ); if (command === "clear") { // Clear everything clearMessages(); } else if (command === "logout") { // Add message, then logout - setMessages((prev: any[]) => [...prev, userMessage]); + setMessages((prev) => [...prev, userMessage]); onLogout(); } else if (command === "help") { // Add user message first - setMessages((prev: any[]) => [...prev, userMessage]); + setMessages((prev) => [...prev, userMessage]); // Create help message with metadata and add after a brief delay for better UX setTimeout(() => { const helpMessageData = createHelpMessage(); - const helpMessage = { - id: (Date.now() + 1).toString(), - role: "system" as const, - content: helpMessageData.content, - createdAt: new Date(), - data: { ...helpMessageData.data, simulateStreaming: true }, - }; + const helpMessage = createUIMessage( + (Date.now() + 1).toString(), + "system", + helpMessageData.content, + { ...helpMessageData.data, simulateStreaming: true }, + ); setMessages((prev) => [...prev, helpMessage]); // Start streaming simulation @@ -269,40 +307,38 @@ Try asking me things like: }, 100); } else if (command === "tools") { // Add user message first - setMessages((prev: any[]) => [...prev, userMessage]); + setMessages((prev) => [...prev, userMessage]); // Create tools message setTimeout(() => { const toolsMessageData = createToolsMessage(); - const toolsMessage = { - id: (Date.now() + 1).toString(), - role: "system" as const, - content: toolsMessageData.content, - createdAt: new Date(), - data: { ...toolsMessageData.data, simulateStreaming: true }, - }; + const toolsMessage = createUIMessage( + (Date.now() + 1).toString(), + "system", + toolsMessageData.content, + { ...toolsMessageData.data, simulateStreaming: true }, + ); setMessages((prev) => [...prev, toolsMessage]); startStreaming(toolsMessage.id, 600); }, 100); } else { // Handle unknown slash commands - add user message and error - const errorMessage = { - id: (Date.now() + 1).toString(), - role: "system" as const, - content: `Unknown command: /${command}. Available commands: /help, /tools, /clear, /logout`, - createdAt: new Date(), - }; + const errorMessage = createUIMessage( + (Date.now() + 1).toString(), + "system", + `Unknown command: /${command}. Available commands: /help, /tools, /clear, /logout`, + ); setMessages((prev) => [...prev, userMessage, errorMessage]); } }, [ clearMessages, onLogout, - setInput, setMessages, createHelpMessage, createToolsMessage, + createUIMessage, startStreaming, ], ); @@ -317,18 +353,10 @@ Try asking me things like: return; } - // Clear the input and directly send the message using append - append({ role: "user", content: prompt }); - }, - [append, handleSlashCommand], - ); - - // Wrap form submission to ensure scrolling - const handleFormSubmit = useCallback( - (e: React.FormEvent) => { - handleSubmit(e); + // Send the message using sendMessage (AI SDK 5+ API) + sendMessage({ text: prompt }); }, - [handleSubmit], + [sendMessage, handleSlashCommand], ); // Show loading state while checking auth session @@ -439,9 +467,9 @@ Try asking me things like: isMessageStreaming={isMessageStreaming} isOpen={isOpen} onInputChange={handleInputChange} - onSubmit={handleFormSubmit} + onSubmit={handleSubmit} onStop={stop} - onRetry={reload} + onRetry={regenerate} onSlashCommand={handleSlashCommand} onSendPrompt={handleSendPrompt} /> diff --git a/packages/mcp-cloudflare/src/client/components/chat/types.ts b/packages/mcp-cloudflare/src/client/components/chat/types.ts index a22d8b15..6a3dee8a 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/types.ts +++ b/packages/mcp-cloudflare/src/client/components/chat/types.ts @@ -3,13 +3,13 @@ */ import type React from "react"; -import type { Message } from "ai/react"; +import type { UIMessage } from "@ai-sdk/react"; -// Re-export AI SDK types for convenience -export type { Message } from "ai/react"; +// Re-export AI SDK types for convenience - Message renamed to UIMessage in AI SDK 5+ +export type { UIMessage as Message } from "@ai-sdk/react"; // Extended message type that includes our custom metadata -export interface ExtendedMessage extends Message { +export interface ExtendedMessage extends UIMessage { data?: { type?: string; prompts?: any[]; @@ -91,7 +91,7 @@ export interface ChatToolInvocation { // Message processing types export interface ProcessedMessagePart { - part: NonNullable[number]; + part: NonNullable[number]; messageId: string; messageRole: string; partIndex: number; @@ -107,7 +107,7 @@ export interface ChatProps { } export interface ChatUIProps { - messages: Message[]; + messages: UIMessage[]; input: string; error?: Error | null; isChatLoading: boolean; @@ -124,7 +124,7 @@ export interface ChatUIProps { } export interface ChatMessagesProps { - messages: Message[]; + messages: UIMessage[]; isChatLoading: boolean; isLocalStreaming?: boolean; isMessageStreaming?: (messageId: string) => boolean; @@ -153,7 +153,7 @@ export interface PanelBackdropProps { } export interface MessagePartProps { - part: NonNullable[number]; + part: NonNullable[number]; messageId: string; messageRole: string; partIndex: number; diff --git a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts index 6281f826..67f99e79 100644 --- a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts +++ b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from "react"; -import type { Message } from "ai"; +import type { UIMessage } from "ai"; const CHAT_STORAGE_KEY = "sentry_chat_messages"; const TIMESTAMP_STORAGE_KEY = "sentry_chat_timestamp"; @@ -31,42 +31,45 @@ export function usePersistedChat(isAuthenticated: boolean) { }, []); // Validate a message to ensure it won't cause conversion errors - const isValidMessage = useCallback((msg: Message): boolean => { - // Check if message has parts (newer structure) + const isValidMessage = useCallback((msg: UIMessage): boolean => { + // UIMessage always has parts array in AI SDK 5+ if (msg.parts && Array.isArray(msg.parts)) { // Check each part for validity - return msg.parts.every((part) => { - // Text parts are always valid - if (part.type === "text") { - return true; - } - - // Tool invocation parts must be complete (have result) if state is "call" or "result" - if (part.type === "tool-invocation") { - const invocation = part as any; - // If it's in "call" or "result" state, it must have a result - if (invocation.state === "call" || invocation.state === "result") { - const content = invocation.result?.content; - // Ensure content exists and is not an empty array - return ( - content && (Array.isArray(content) ? content.length > 0 : true) - ); + return msg.parts.every( + (part: { + type: string; + text?: string; + state?: string; + result?: { content?: unknown }; + }) => { + // Text parts are always valid + if (part.type === "text") { + return true; } - // partial-call state is okay without result - return true; - } - // Other part types are assumed valid - return true; - }); - } + // Tool invocation parts must be complete (have result) if state is "call" or "result" + if (part.type === "tool-invocation") { + const invocation = part as any; + // If it's in "call" or "result" state, it must have a result + if (invocation.state === "call" || invocation.state === "result") { + const content = invocation.result?.content; + // Ensure content exists and is not an empty array + return ( + content && (Array.isArray(content) ? content.length > 0 : true) + ); + } + // partial-call state is okay without result + return true; + } - // Check if message has content (legacy structure) - if (msg.content && typeof msg.content === "string") { - return msg.content.trim() !== ""; + // Other part types are assumed valid + return true; + }, + ); } - return false; + // UIMessage should always have parts, but check for empty + return msg.parts && msg.parts.length > 0; }, []); // Load initial messages from localStorage @@ -84,7 +87,7 @@ export function usePersistedChat(isAuthenticated: boolean) { try { const stored = localStorage.getItem(CHAT_STORAGE_KEY); if (stored) { - const parsed = JSON.parse(stored) as Message[]; + const parsed = JSON.parse(stored) as UIMessage[]; // Validate the data structure if (Array.isArray(parsed) && parsed.length > 0) { // Filter out any invalid or incomplete messages @@ -108,7 +111,7 @@ export function usePersistedChat(isAuthenticated: boolean) { // Function to save messages const saveMessages = useCallback( - (messages: Message[]) => { + (messages: UIMessage[]) => { if (!isAuthenticated || messages.length === 0) return; try { diff --git a/packages/mcp-cloudflare/src/server/routes/chat.ts b/packages/mcp-cloudflare/src/server/routes/chat.ts index 7daee4cb..0247303f 100644 --- a/packages/mcp-cloudflare/src/server/routes/chat.ts +++ b/packages/mcp-cloudflare/src/server/routes/chat.ts @@ -325,7 +325,7 @@ Start conversations by exploring what's available in their account. Use tools li - \`get_issue_details\` to dive deep into specific errors Remember: You're a test assistant, not a general-purpose helper. Stay focused on testing the MCP integration with their real data.`, - maxTokens: 2000, + maxOutputTokens: 2000, stopWhen: stepCountIs(10), experimental_telemetry: { isEnabled: true, @@ -333,7 +333,7 @@ Remember: You're a test assistant, not a general-purpose helper. Stay focused on }); // Clean up MCP client when the response stream ends - const response = result.toDataStreamResponse(); + const response = result.toUIMessageStreamResponse(); // Note: In a production environment, you might want to implement proper cleanup // This is a simplified approach for the demo diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b65f46b1..de110f35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,9 +27,6 @@ catalogs: '@cloudflare/workers-types': specifier: ^4.20251014.0 version: 4.20251014.0 - '@modelcontextprotocol/sdk': - specifier: ^1.25.3 - version: 1.25.3 '@radix-ui/react-accordion': specifier: ^1.2.11 version: 1.2.11 @@ -169,6 +166,9 @@ catalogs: specifier: ^3.24.6 version: 3.25.0 +overrides: + '@modelcontextprotocol/sdk': ^1.25.3 + importers: .: @@ -232,7 +232,7 @@ importers: specifier: 'catalog:' version: 0.0.12 '@modelcontextprotocol/sdk': - specifier: 'catalog:' + specifier: ^1.25.3 version: 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) '@radix-ui/react-accordion': specifier: 'catalog:' @@ -371,7 +371,7 @@ importers: specifier: ^1.1.1 version: 1.1.1(@logtape/logtape@1.1.1) '@modelcontextprotocol/sdk': - specifier: 'catalog:' + specifier: ^1.25.3 version: 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) '@sentry/core': specifier: 'catalog:' @@ -414,7 +414,7 @@ importers: packages/mcp-server: dependencies: '@modelcontextprotocol/sdk': - specifier: 'catalog:' + specifier: ^1.25.3 version: 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) '@sentry/core': specifier: 'catalog:' @@ -463,7 +463,7 @@ importers: specifier: 'catalog:' version: 3.0.23(zod@3.25.76) '@modelcontextprotocol/sdk': - specifier: 'catalog:' + specifier: ^1.25.3 version: 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) '@sentry/mcp-core': specifier: workspace:* @@ -523,7 +523,7 @@ importers: specifier: 'catalog:' version: 3.0.23(zod@3.25.76) '@modelcontextprotocol/sdk': - specifier: 'catalog:' + specifier: ^1.25.3 version: 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) '@sentry/core': specifier: 'catalog:' @@ -1401,16 +1401,6 @@ packages: peerDependencies: '@logtape/logtape': ^1.1.1 - '@modelcontextprotocol/sdk@1.25.2': - resolution: {integrity: sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - '@modelcontextprotocol/sdk@1.25.3': resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==} engines: {node: '>=18'} @@ -5660,30 +5650,6 @@ snapshots: '@logtape/logtape': 1.1.1 '@sentry/core': 9.34.0 - '@modelcontextprotocol/sdk@1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76)': - dependencies: - '@hono/node-server': 1.19.9(hono@4.11.4) - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.5 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.1.0 - express-rate-limit: 7.5.1(express@5.1.0) - jose: 6.1.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.0 - raw-body: 3.0.1 - zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) - optionalDependencies: - '@cfworker/json-schema': 4.1.1 - transitivePeerDependencies: - - hono - - supports-color - '@modelcontextprotocol/sdk@1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.9(hono@4.11.4) @@ -6737,7 +6703,7 @@ snapshots: '@cfworker/json-schema': 4.1.1 '@cloudflare/ai-chat': 0.0.4(agents@0.3.6)(ai@6.0.64(zod@3.25.76))(react@19.1.0)(zod@3.25.76) '@cloudflare/codemode': 0.0.5(agents@0.3.6)(ai@6.0.64(zod@3.25.76))(typescript@5.8.3)(zod@3.25.76) - '@modelcontextprotocol/sdk': 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) ai: 6.0.64(zod@3.25.76) cron-schedule: 6.0.0 json-schema: 0.4.0 From b78ef9a462ce0e14aeb194c128059ab5ae1f3937 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 30 Jan 2026 21:26:00 -0800 Subject: [PATCH 04/12] fix(cloudflare): Fix remaining AI SDK 6.x type compatibility issues Additional fixes for UIMessage type changes in AI SDK 5+/6.x: - Remove `content` property usage (UIMessage always uses `parts` array) - Replace `data` with `metadata` for custom message metadata - Remove `createdAt` property (no longer in UIMessage interface) - Update tool part handling to support AI SDK 6 format where tool type is `tool-${toolName}` with properties directly on the part - Remove legacy content fallback in processMessages Co-Authored-By: Claude Opus 4.5 --- .../client/components/chat/chat-message.tsx | 66 ++++++++++++------- .../client/components/chat/chat-messages.tsx | 26 +------- .../src/client/components/chat/chat.tsx | 5 +- .../src/client/hooks/use-persisted-chat.ts | 2 +- 4 files changed, 48 insertions(+), 51 deletions(-) diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx index 79c78802..b4d67953 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx @@ -127,6 +127,13 @@ const ToolPart = memo(function ToolPart({ ); }); +// Helper to check if a part is a tool part (type starts with "tool-") +const isToolPart = (part: { type: string }): part is { + type: `tool-${string}`; +} & ChatToolInvocation => { + return part.type.startsWith("tool-") && part.type !== "tool-invocation"; +}; + // Main component for rendering individual message parts const MessagePart = memo(function MessagePart({ part, @@ -137,30 +144,43 @@ const MessagePart = memo(function MessagePart({ messageData, onSlashCommand, }: MessagePartProps) { - switch (part.type) { - case "text": - return ( - - ); - case "tool-invocation": - return ( - - ); - default: - // Fallback for unknown part types - return null; + // Handle text parts + if (part.type === "text") { + return ( + + ); } + + // Handle tool parts (AI SDK 6 format: type is "tool-${toolName}") + if (isToolPart(part)) { + // Convert AI SDK 6 tool part to our ChatToolInvocation format + const toolInvocation: ChatToolInvocation = { + toolCallId: part.toolCallId, + toolName: part.type.replace(/^tool-/, ""), + args: (part as any).input ?? {}, + state: (part as any).state === "result" ? "result" : "call", + result: (part as any).output + ? { content: [{ type: "text", text: String((part as any).output) }] } + : undefined, + }; + return ( + + ); + } + + // Fallback for unknown part types + return null; }); // Export the memoized components diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat-messages.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat-messages.tsx index 46c06fe2..8f76e02a 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat-messages.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat-messages.tsx @@ -10,9 +10,6 @@ import { useAuth } from "../../contexts/auth-context"; // Import UIMessage from our types (re-exported as Message for compatibility) import type { UIMessage } from "@ai-sdk/react"; -// Cache for stable part objects to avoid recreating them -const partCache = new WeakMap(); - function processMessages( messages: UIMessage[], isChatLoading: boolean, @@ -50,25 +47,6 @@ function processMessages( }); }, ); - } else if (message.content) { - // Use cached part object to maintain stable references - let part = partCache.get(message); - if (!part) { - part = { type: "text", text: message.content }; - partCache.set(message, part); - } - - allParts.push({ - part, - messageId: message.id, - messageRole: message.role, - partIndex: 0, - // Stream if it's AI response OR local streaming simulation - isStreaming: - (isLastMessage && isChatLoading) || - isMessageStreaming?.(message.id) || - false, - }); } }); @@ -137,7 +115,7 @@ export function ChatMessages({ const originalMessage = messages.find( (m) => m.id === item.messageId, ); - const messageData = originalMessage?.data as any; + const messageData = originalMessage?.metadata as any; const hasToolActions = messageData?.type === "tools-list" && messageData?.toolsDetailed && @@ -151,7 +129,7 @@ export function ChatMessages({ messageRole={item.messageRole} partIndex={item.partIndex} isStreaming={item.isStreaming} - messageData={originalMessage?.data} + messageData={originalMessage?.metadata} onSlashCommand={onSlashCommand} /> {/* Show tool actions list for tools-list messages */} diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat.tsx index 45284392..cdf52814 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat.tsx @@ -256,13 +256,12 @@ Try asking me things like: id: string, role: "user" | "assistant" | "system", text: string, - data?: Record, + metadata?: Record, ): UIMessage => ({ id, role, parts: [{ type: "text", text }], - createdAt: new Date(), - ...(data && { data }), + ...(metadata && { metadata }), }), [], ); diff --git a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts index 67f99e79..88f4de97 100644 --- a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts +++ b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts @@ -69,7 +69,7 @@ export function usePersistedChat(isAuthenticated: boolean) { } // UIMessage should always have parts, but check for empty - return msg.parts && msg.parts.length > 0; + return Boolean(msg.parts && msg.parts.length > 0); }, []); // Load initial messages from localStorage From 4f13ba6f0b89249c1c7313153b1dfccc1bdffee7 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 30 Jan 2026 21:30:34 -0800 Subject: [PATCH 05/12] fix(cloudflare): Fix TypeScript narrowing error in isValidMessage Refactored the isValidMessage callback to use early return pattern, which fixes the TypeScript error where msg.parts was narrowed to type 'never' in the else branch. Since UIMessage.parts is a required property, the previous if/else structure was flagged by TypeScript as having unreachable code. Co-Authored-By: Claude Opus 4.5 --- .../src/client/hooks/use-persisted-chat.ts | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts index 88f4de97..2ae9dadf 100644 --- a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts +++ b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts @@ -32,44 +32,44 @@ export function usePersistedChat(isAuthenticated: boolean) { // Validate a message to ensure it won't cause conversion errors const isValidMessage = useCallback((msg: UIMessage): boolean => { - // UIMessage always has parts array in AI SDK 5+ - if (msg.parts && Array.isArray(msg.parts)) { - // Check each part for validity - return msg.parts.every( - (part: { - type: string; - text?: string; - state?: string; - result?: { content?: unknown }; - }) => { - // Text parts are always valid - if (part.type === "text") { - return true; - } + // UIMessage always has parts array in AI SDK 6+ + // Check that parts exists and is non-empty + if (!msg.parts || !Array.isArray(msg.parts) || msg.parts.length === 0) { + return false; + } - // Tool invocation parts must be complete (have result) if state is "call" or "result" - if (part.type === "tool-invocation") { - const invocation = part as any; - // If it's in "call" or "result" state, it must have a result - if (invocation.state === "call" || invocation.state === "result") { - const content = invocation.result?.content; - // Ensure content exists and is not an empty array - return ( - content && (Array.isArray(content) ? content.length > 0 : true) - ); - } - // partial-call state is okay without result - return true; - } + // Check each part for validity + return msg.parts.every( + (part: { + type: string; + text?: string; + state?: string; + result?: { content?: unknown }; + }) => { + // Text parts are always valid + if (part.type === "text") { + return true; + } - // Other part types are assumed valid + // Tool invocation parts must be complete (have result) if state is "call" or "result" + if (part.type === "tool-invocation") { + const invocation = part as any; + // If it's in "call" or "result" state, it must have a result + if (invocation.state === "call" || invocation.state === "result") { + const content = invocation.result?.content; + // Ensure content exists and is not an empty array + return ( + content && (Array.isArray(content) ? content.length > 0 : true) + ); + } + // partial-call state is okay without result return true; - }, - ); - } + } - // UIMessage should always have parts, but check for empty - return Boolean(msg.parts && msg.parts.length > 0); + // Other part types are assumed valid + return true; + }, + ); }, []); // Load initial messages from localStorage From ecd210a1732a9ad70f397065d153cf79deedb1d3 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 30 Jan 2026 21:48:20 -0800 Subject: [PATCH 06/12] fix(cloudflare): Handle legacy tool-invocation and fix output conversion Addresses Cursor Bugbot feedback: 1. Legacy tool-invocation parts from persisted messages (AI SDK 4/5 format) now render correctly instead of being silently ignored 2. Tool output conversion now properly handles MCP-format objects with content arrays, strings, and other objects - avoiding [object Object] Co-Authored-By: Claude Opus 4.5 --- .../client/components/chat/chat-message.tsx | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx index b4d67953..023e8f5b 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx @@ -127,13 +127,46 @@ const ToolPart = memo(function ToolPart({ ); }); -// Helper to check if a part is a tool part (type starts with "tool-") +// Helper to check if a part is an AI SDK 6 tool part (type starts with "tool-") const isToolPart = (part: { type: string }): part is { type: `tool-${string}`; } & ChatToolInvocation => { return part.type.startsWith("tool-") && part.type !== "tool-invocation"; }; +// Helper to check if a part is a legacy tool-invocation part (AI SDK 4/5 format) +const isLegacyToolInvocation = (part: { type: string }): part is { + type: "tool-invocation"; +} & ChatToolInvocation => { + return part.type === "tool-invocation"; +}; + +// Helper to convert tool output to proper content format +const convertToolOutput = ( + output: unknown, +): { content: Array<{ type: "text"; text: string }> } | undefined => { + if (output === undefined || output === null) { + return undefined; + } + + // If output is already in MCP format with content array + if ( + typeof output === "object" && + "content" in (output as object) && + Array.isArray((output as { content: unknown }).content) + ) { + return output as { content: Array<{ type: "text"; text: string }> }; + } + + // If output is a string, wrap it + if (typeof output === "string") { + return { content: [{ type: "text", text: output }] }; + } + + // For other objects, JSON stringify + return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] }; +}; + // Main component for rendering individual message parts const MessagePart = memo(function MessagePart({ part, @@ -158,6 +191,19 @@ const MessagePart = memo(function MessagePart({ ); } + // Handle legacy tool-invocation parts (AI SDK 4/5 format from persisted messages) + if (isLegacyToolInvocation(part)) { + // Legacy format already has the ChatToolInvocation structure + const legacyPart = part as unknown as ChatToolInvocation; + return ( + + ); + } + // Handle tool parts (AI SDK 6 format: type is "tool-${toolName}") if (isToolPart(part)) { // Convert AI SDK 6 tool part to our ChatToolInvocation format @@ -166,9 +212,7 @@ const MessagePart = memo(function MessagePart({ toolName: part.type.replace(/^tool-/, ""), args: (part as any).input ?? {}, state: (part as any).state === "result" ? "result" : "call", - result: (part as any).output - ? { content: [{ type: "text", text: String((part as any).output) }] } - : undefined, + result: convertToolOutput((part as any).output), }; return ( Date: Fri, 30 Jan 2026 22:02:30 -0800 Subject: [PATCH 07/12] fix(cloudflare): Access toolInvocation property for legacy tool parts Legacy AI SDK 4/5 tool-invocation parts have structure: { type: "tool-invocation", toolInvocation: {...} } The previous fix incorrectly cast the entire part to ChatToolInvocation instead of accessing part.toolInvocation. This caused undefined values for toolName, toolCallId, args, state, and result. Co-Authored-By: Claude Opus 4.5 --- .../src/client/components/chat/chat-message.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx index 023e8f5b..bb76d750 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx @@ -135,10 +135,16 @@ const isToolPart = (part: { type: string }): part is { }; // Helper to check if a part is a legacy tool-invocation part (AI SDK 4/5 format) +// Legacy format: { type: "tool-invocation", toolInvocation: ChatToolInvocation } const isLegacyToolInvocation = (part: { type: string }): part is { type: "tool-invocation"; -} & ChatToolInvocation => { - return part.type === "tool-invocation"; + toolInvocation: ChatToolInvocation; +} => { + return ( + part.type === "tool-invocation" && + "toolInvocation" in part && + typeof (part as any).toolInvocation === "object" + ); }; // Helper to convert tool output to proper content format @@ -192,12 +198,11 @@ const MessagePart = memo(function MessagePart({ } // Handle legacy tool-invocation parts (AI SDK 4/5 format from persisted messages) + // Legacy format: { type: "tool-invocation", toolInvocation: {...} } if (isLegacyToolInvocation(part)) { - // Legacy format already has the ChatToolInvocation structure - const legacyPart = part as unknown as ChatToolInvocation; return ( From 5560ac250701cb4330c6b785c2cb4a66b098809a Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sat, 31 Jan 2026 08:43:26 -0800 Subject: [PATCH 08/12] fix(cloudflare): Migrate legacy AI SDK 4.x messages to new parts format Added migration logic to convert persisted messages from AI SDK 4.x format (using `content` string) to AI SDK 6 format (using `parts` array). This prevents silent data loss when users upgrade, as their chat history will be automatically migrated instead of being filtered out. Co-Authored-By: Claude Opus 4.5 --- .../src/client/hooks/use-persisted-chat.ts | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts index 2ae9dadf..01cdd54e 100644 --- a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts +++ b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts @@ -6,6 +6,34 @@ const TIMESTAMP_STORAGE_KEY = "sentry_chat_timestamp"; const MAX_STORED_MESSAGES = 100; // Limit storage size const CACHE_EXPIRY_MS = 60 * 60 * 1000; // 1 hour in milliseconds +// Legacy AI SDK 4.x message format (before migration to parts-based format) +interface LegacyMessage { + id: string; + role: "user" | "assistant" | "system"; + content?: string; + parts?: UIMessage["parts"]; +} + +// Migrate legacy messages (AI SDK 4.x format with `content`) to new format (with `parts`) +function migrateMessage(msg: LegacyMessage): UIMessage { + // If message already has parts, use it as-is + if (msg.parts && Array.isArray(msg.parts) && msg.parts.length > 0) { + return msg as UIMessage; + } + + // If message has legacy content string, convert to parts format + if (typeof msg.content === "string" && msg.content.length > 0) { + return { + id: msg.id, + role: msg.role, + parts: [{ type: "text", text: msg.content }], + } as UIMessage; + } + + // Return as-is if neither format matches (will be filtered by validation) + return msg as UIMessage; +} + export function usePersistedChat(isAuthenticated: boolean) { // Check if cache is expired const isCacheExpired = useCallback(() => { @@ -87,11 +115,12 @@ export function usePersistedChat(isAuthenticated: boolean) { try { const stored = localStorage.getItem(CHAT_STORAGE_KEY); if (stored) { - const parsed = JSON.parse(stored) as UIMessage[]; + const parsed = JSON.parse(stored) as LegacyMessage[]; // Validate the data structure if (Array.isArray(parsed) && parsed.length > 0) { - // Filter out any invalid or incomplete messages - const validMessages = parsed.filter(isValidMessage); + // Migrate legacy messages and filter out any invalid ones + const migratedMessages = parsed.map(migrateMessage); + const validMessages = migratedMessages.filter(isValidMessage); if (validMessages.length > 0) { // Update timestamp since we're loading existing messages updateTimestamp(); From dd113ec5531d5c850f86edd8c30e84cdaf6542fc Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sat, 31 Jan 2026 08:56:42 -0800 Subject: [PATCH 09/12] fix(cloudflare): Handle partial-call state and preserve metadata in migration 1. Fixed state mapping for AI SDK 6 tool parts to correctly handle 'partial-call' state during streaming, preventing message persistence failures and incorrect UI rendering. 2. Updated migration function to preserve metadata from both legacy 'data' property and new 'metadata' property, preventing loss of custom properties like type, hasSlashCommands, simulateStreaming. Co-Authored-By: Claude Opus 4.5 --- .../src/client/components/chat/chat-message.tsx | 11 ++++++++++- .../src/client/hooks/use-persisted-chat.ts | 12 +++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx index bb76d750..d6a74ee3 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx @@ -211,12 +211,21 @@ const MessagePart = memo(function MessagePart({ // Handle tool parts (AI SDK 6 format: type is "tool-${toolName}") if (isToolPart(part)) { + // Map AI SDK 6 state to our ChatToolInvocation state + const partState = (part as any).state; + const mappedState: "partial-call" | "call" | "result" = + partState === "result" + ? "result" + : partState === "partial-call" + ? "partial-call" + : "call"; + // Convert AI SDK 6 tool part to our ChatToolInvocation format const toolInvocation: ChatToolInvocation = { toolCallId: part.toolCallId, toolName: part.type.replace(/^tool-/, ""), args: (part as any).input ?? {}, - state: (part as any).state === "result" ? "result" : "call", + state: mappedState, result: convertToolOutput((part as any).output), }; return ( diff --git a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts index 01cdd54e..a3c0c687 100644 --- a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts +++ b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts @@ -12,12 +12,21 @@ interface LegacyMessage { role: "user" | "assistant" | "system"; content?: string; parts?: UIMessage["parts"]; + // Legacy used 'data', new SDK uses 'metadata' + data?: Record; + metadata?: Record; } // Migrate legacy messages (AI SDK 4.x format with `content`) to new format (with `parts`) function migrateMessage(msg: LegacyMessage): UIMessage { - // If message already has parts, use it as-is + // Preserve metadata from either 'metadata' or legacy 'data' property + const metadata = msg.metadata ?? msg.data; + + // If message already has parts, use it as-is (but ensure metadata is preserved) if (msg.parts && Array.isArray(msg.parts) && msg.parts.length > 0) { + if (metadata) { + return { ...msg, metadata } as UIMessage; + } return msg as UIMessage; } @@ -27,6 +36,7 @@ function migrateMessage(msg: LegacyMessage): UIMessage { id: msg.id, role: msg.role, parts: [{ type: "text", text: msg.content }], + ...(metadata && { metadata }), } as UIMessage; } From 39de412f32c24191c7b7508fa60197c4c226863e Mon Sep 17 00:00:00 2001 From: David Cramer Date: Tue, 3 Feb 2026 09:07:50 -0800 Subject: [PATCH 10/12] fix(cloudflare): Convert UIMessage to ModelMessage before streamText The AI SDK 6.x has two different message formats: - UIMessage (client/UI format) uses a `parts` array - ModelMessage (API format) uses a `content` property The chat endpoint was passing UIMessage[] directly to streamText(), which expects ModelMessage[]. Added convertToModelMessages() to properly convert the format before streaming. Co-Authored-By: Claude Opus 4.5 --- packages/mcp-cloudflare/src/server/routes/chat.ts | 15 +++++++++++++-- packages/mcp-cloudflare/src/server/types/chat.ts | 9 +++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/mcp-cloudflare/src/server/routes/chat.ts b/packages/mcp-cloudflare/src/server/routes/chat.ts index 0247303f..a32b11cc 100644 --- a/packages/mcp-cloudflare/src/server/routes/chat.ts +++ b/packages/mcp-cloudflare/src/server/routes/chat.ts @@ -1,6 +1,11 @@ import { Hono, type Context } from "hono"; import { openai } from "@ai-sdk/openai"; -import { streamText, type ToolSet, stepCountIs } from "ai"; +import { + streamText, + type ToolSet, + stepCountIs, + convertToModelMessages, +} from "ai"; import { experimental_createMCPClient } from "@ai-sdk/mcp"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import type { Env } from "../types"; @@ -299,9 +304,15 @@ export default new Hono<{ Bindings: Env }>().post("/", async (c) => { } } + // Convert UIMessage[] (parts-based) to ModelMessage[] (content-based) for streamText + const modelMessages = await convertToModelMessages(messages, { + tools, + ignoreIncompleteToolCalls: true, + }); + const result = streamText({ model: openai("gpt-4o"), - messages, + messages: modelMessages, tools, system: `You are an AI assistant designed EXCLUSIVELY for testing the Sentry MCP service. Your sole purpose is to help users test MCP functionality with their real Sentry account data - nothing more, nothing less. diff --git a/packages/mcp-cloudflare/src/server/types/chat.ts b/packages/mcp-cloudflare/src/server/types/chat.ts index 810d8633..987acb64 100644 --- a/packages/mcp-cloudflare/src/server/types/chat.ts +++ b/packages/mcp-cloudflare/src/server/types/chat.ts @@ -2,6 +2,7 @@ * Type definitions for Chat API */ import { z } from "zod"; +import type { UIMessage } from "ai"; // Shared schemas for authentication data across chat routes export const AuthDataSchema = z.object({ @@ -51,13 +52,9 @@ export interface ErrorResponse { eventId?: string; } -// Request types +// Request types - uses UIMessage format from AI SDK 6.x (parts-based) export interface ChatRequest { - messages: Array<{ - role: "user" | "assistant" | "system"; - content: string; - data?: any; // Additional metadata for messages - }>; + messages: UIMessage[]; } // MCP types From aba3dcc6ddfb2623ccd098dd12e56618fe33399d Mon Sep 17 00:00:00 2001 From: David Cramer Date: Tue, 3 Feb 2026 09:35:43 -0800 Subject: [PATCH 11/12] fix(cloudflare): Reset chat on mode switch and validate SDK 6.x tool parts Two bug fixes: 1. useChat transport not re-initializing when endpoint mode changes: The useChat hook captures the initial transport and doesn't reset when the prop changes. Added effect to clear messages when mode switches, ensuring the new transport is used for subsequent messages. 2. isValidMessage not validating AI SDK 6.x tool format: AI SDK 6.x uses "tool-" format (e.g., "tool-whoami") instead of "tool-invocation". Updated validation to filter out incomplete tool calls (input-streaming, input-available, approval-requested states) from persisted messages. Also consolidated the auth logout and mode change effects into one. Co-Authored-By: Claude Opus 4.5 --- .../src/client/components/chat/chat.tsx | 20 +++--- .../src/client/hooks/use-persisted-chat.ts | 67 ++++++++++--------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat.tsx index cdf52814..50064f83 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat.tsx @@ -198,21 +198,23 @@ Try asking me things like: }; }, []); - // Track previous auth state to detect logout events - const prevAuthStateRef = useRef(isAuthenticated); + // Track previous auth and endpoint mode to detect changes requiring message clearing + const prevStateRef = useRef({ isAuthenticated, endpointMode }); - // Clear messages when user logs out (auth state changes from authenticated to not) + // Clear messages when user logs out or endpoint mode changes useEffect(() => { - const wasAuthenticated = prevAuthStateRef.current; + const prev = prevStateRef.current; - // Detect logout: was authenticated but now isn't - if (wasAuthenticated && !isAuthenticated) { + // Clear on logout (was authenticated but now isn't) or endpoint mode change + const didLogout = prev.isAuthenticated && !isAuthenticated; + const didChangeMode = prev.endpointMode !== endpointMode; + + if (didLogout || didChangeMode) { clearMessages(); } - // Update the ref for next comparison - prevAuthStateRef.current = isAuthenticated; - }, [isAuthenticated, clearMessages]); + prevStateRef.current = { isAuthenticated, endpointMode }; + }, [isAuthenticated, endpointMode, clearMessages]); // Save messages when they change useEffect(() => { diff --git a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts index a3c0c687..8e2ae72f 100644 --- a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts +++ b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts @@ -71,43 +71,50 @@ export function usePersistedChat(isAuthenticated: boolean) { // Validate a message to ensure it won't cause conversion errors const isValidMessage = useCallback((msg: UIMessage): boolean => { // UIMessage always has parts array in AI SDK 6+ - // Check that parts exists and is non-empty if (!msg.parts || !Array.isArray(msg.parts) || msg.parts.length === 0) { return false; } - // Check each part for validity - return msg.parts.every( - (part: { - type: string; - text?: string; - state?: string; - result?: { content?: unknown }; - }) => { - // Text parts are always valid - if (part.type === "text") { - return true; - } + // Invalid states that indicate incomplete tool calls + const incompleteToolStates = new Set([ + "input-streaming", + "input-available", + "approval-requested", + ]); - // Tool invocation parts must be complete (have result) if state is "call" or "result" - if (part.type === "tool-invocation") { - const invocation = part as any; - // If it's in "call" or "result" state, it must have a result - if (invocation.state === "call" || invocation.state === "result") { - const content = invocation.result?.content; - // Ensure content exists and is not an empty array - return ( - content && (Array.isArray(content) ? content.length > 0 : true) - ); - } - // partial-call state is okay without result - return true; - } + return msg.parts.every((part) => { + // Text parts are always valid + if (part.type === "text") { + return true; + } - // Other part types are assumed valid + // AI SDK 6.x uses "tool-" format (e.g., "tool-whoami") + // Filter out incomplete tool calls that shouldn't be persisted + if (part.type.startsWith("tool-")) { + const { state } = part as { state?: string }; + return !incompleteToolStates.has(state ?? ""); + } + + // Legacy "tool-invocation" format (AI SDK 4.x) + if (part.type === "tool-invocation") { + const { state, result } = part as { + state?: string; + result?: { content?: unknown }; + }; + // "call" or "result" state requires valid content + if (state === "call" || state === "result") { + const content = result?.content; + return ( + content != null && (!Array.isArray(content) || content.length > 0) + ); + } + // partial-call state is okay without result return true; - }, - ); + } + + // Other part types (reasoning, file, source-url, etc.) are valid + return true; + }); }, []); // Load initial messages from localStorage From a333923d7d3c87da45d84d8c858635930a931136 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Tue, 3 Feb 2026 09:42:34 -0800 Subject: [PATCH 12/12] fix(cloudflare): Access toolInvocation property for legacy validation Legacy AI SDK 4.x tool-invocation parts have structure: { type: "tool-invocation", toolInvocation: { state, result, ... } } The validation was incorrectly accessing state and result directly on the part object instead of through the nested toolInvocation property. This caused all legacy tool-invocation parts to pass validation regardless of their actual state. Co-Authored-By: Claude Opus 4.5 --- .../src/client/hooks/use-persisted-chat.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts index 8e2ae72f..10b0ef08 100644 --- a/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts +++ b/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts @@ -96,14 +96,20 @@ export function usePersistedChat(isAuthenticated: boolean) { } // Legacy "tool-invocation" format (AI SDK 4.x) + // Structure: { type: "tool-invocation", toolInvocation: { state, result, ... } } if (part.type === "tool-invocation") { - const { state, result } = part as { - state?: string; - result?: { content?: unknown }; - }; + const invocation = ( + part as { + toolInvocation?: { + state?: string; + result?: { content?: unknown }; + }; + } + ).toolInvocation; + if (!invocation) return true; // No invocation data, allow it // "call" or "result" state requires valid content - if (state === "call" || state === "result") { - const content = result?.content; + if (invocation.state === "call" || invocation.state === "result") { + const content = invocation.result?.content; return ( content != null && (!Array.isArray(content) || content.length > 0) );