feat: add support for AI SDK v5 dynamic tools#589
Conversation
- Update AI SDK to v5.0.0-beta.29 and @ai-sdk/react to v2.0.0-beta.29 - Rename MCP-specific columns to generic dynamic tool columns in database schema - Add tool_dynamic_name and tool_dynamic_type columns for better tool identification - Update message mapping to handle dynamic-tool UI parts from AI SDK v5 - Create DynamicToolDisplay component for rendering dynamic tool invocations - Fix trigger names (regenerate-message, submit-message) for new SDK version - Update addToolResult to include required 'tool' parameter - Support MCP tools, user-defined functions, and external API tools at runtime
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
There was a problem hiding this comment.
Pull Request Overview
This PR adds support for AI SDK v5's dynamic tools feature, enabling runtime-defined tools beyond compile-time static tools. It includes database schema changes to support dynamic tool metadata, updates to message mapping for the new SDK format, and new UI components for rendering dynamic tool interactions.
- Upgraded AI SDK packages to v5.0.0-beta.29 with new dynamic tool APIs
- Refactored database schema from MCP-specific to generic dynamic tool support
- Updated message handling to support AI SDK v5's
dynamic-toolUI parts
Reviewed Changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Updates AI SDK packages to v5.0.0-beta.29 |
| lib/utils/message-mapping.ts | Adds mapping logic for dynamic-tool parts and metadata handling |
| lib/types/message-persistence.ts | Renames MCP types to dynamic tool types |
| lib/types/ai.ts | Adds flexibility for dynamic tools in UITools interface |
| lib/tools/dynamic.ts | New dynamic tool creation utilities |
| lib/db/schema.ts | Renames MCP columns to dynamic tool columns with metadata |
| lib/db/migrations/0003_rename_mcp_to_dynamic.sql | Migration script for schema changes |
| lib/agents/researcher-with-dynamic-tools.ts | New agent supporting dynamic tools |
| drizzle/meta/_journal.json | Migration metadata updates |
| drizzle/0001_rename_mcp_to_dynamic.sql | Drizzle migration file |
| components/render-message.tsx | Adds rendering support for dynamic-tool parts |
| components/dynamic-tool-display.tsx | New component for displaying dynamic tool interactions |
| components/chat.tsx | Updates trigger names and addToolResult signature |
lib/utils/message-mapping.ts
Outdated
| const toolInputColumn = `tool_${toolName}_input` as keyof DBMessagePart | ||
|
|
||
| return { | ||
| const result: any = { |
There was a problem hiding this comment.
Using 'any' type reduces type safety. Consider defining a specific interface for the result object or using a more specific type.
| const result: any = { | |
| const result: ToolCallResult = { |
lib/utils/message-mapping.ts
Outdated
| `tool_${resultToolName}_output` as keyof DBMessagePart | ||
|
|
||
| return { | ||
| const toolResult: any = { |
There was a problem hiding this comment.
Using 'any' type reduces type safety. Consider defining a specific interface for the toolResult object or using a more specific type.
| const toolResult: any = { | |
| const toolResult: ToolResult = { |
lib/utils/message-mapping.ts
Outdated
|
|
||
| // Dynamic tool parts from AI SDK v5 | ||
| case 'dynamic-tool': | ||
| const dynamicPart = part as any |
There was a problem hiding this comment.
Using 'as any' type assertion bypasses type checking. Consider defining a proper interface for dynamic parts or using type guards to ensure type safety.
lib/tools/dynamic.ts
Outdated
| export function createMCPTool( | ||
| toolName: string, | ||
| description: string, | ||
| mcpClient: any // Replace with actual MCP client type |
There was a problem hiding this comment.
Using 'any' type for mcpClient parameter reduces type safety. Consider defining a proper interface for the MCP client.
| mcpClient: any // Replace with actual MCP client type | |
| mcpClient: MCPClient // Replace with actual MCP client type |
| interface DynamicToolConfig { | ||
| name: string | ||
| description: string | ||
| handler?: (params: any) => Promise<any> | ||
| mcpClient?: any // Replace with actual MCP client type |
There was a problem hiding this comment.
Using 'any' type for mcpClient reduces type safety. Consider defining a proper interface for the MCP client.
| interface DynamicToolConfig { | |
| name: string | |
| description: string | |
| handler?: (params: any) => Promise<any> | |
| mcpClient?: any // Replace with actual MCP client type | |
| interface MCPClient { | |
| connect(): Promise<void>; | |
| disconnect(): Promise<void>; | |
| sendRequest(request: object): Promise<object>; | |
| } | |
| interface DynamicToolConfig { | |
| name: string | |
| description: string | |
| handler?: (params: any) => Promise<any> | |
| mcpClient?: MCPClient // Replace with actual MCP client type |
components/render-message.tsx
Outdated
| return ( | ||
| <DynamicToolDisplay | ||
| key={`${messageId}-dynamic-tool-${index}`} | ||
| part={part as any} |
There was a problem hiding this comment.
Using 'as any' type assertion bypasses type checking. Consider defining a proper type for the dynamic tool part or using type guards.
components/chat.tsx
Outdated
| const allParts = messages.flatMap(m => m.parts || []) | ||
|
|
||
| for (const part of allParts) { | ||
| const p = part as any | ||
| if (p.type === 'tool-call' && p.toolCallId === toolCallId) { | ||
| toolName = p.toolName | ||
| break | ||
| } else if ( | ||
| p.type?.startsWith('tool-') && | ||
| p.toolCallId === toolCallId | ||
| ) { | ||
| toolName = p.type.substring(5) // Remove 'tool-' prefix | ||
| break | ||
| } else if ( | ||
| p.type === 'dynamic-tool' && | ||
| p.toolCallId === toolCallId | ||
| ) { | ||
| toolName = p.toolName | ||
| break | ||
| } |
There was a problem hiding this comment.
The flatMap operation creates a new array of all parts on every tool result call. Consider optimizing this by breaking early once the tool is found or caching the parts lookup.
| const allParts = messages.flatMap(m => m.parts || []) | |
| for (const part of allParts) { | |
| const p = part as any | |
| if (p.type === 'tool-call' && p.toolCallId === toolCallId) { | |
| toolName = p.toolName | |
| break | |
| } else if ( | |
| p.type?.startsWith('tool-') && | |
| p.toolCallId === toolCallId | |
| ) { | |
| toolName = p.type.substring(5) // Remove 'tool-' prefix | |
| break | |
| } else if ( | |
| p.type === 'dynamic-tool' && | |
| p.toolCallId === toolCallId | |
| ) { | |
| toolName = p.toolName | |
| break | |
| } | |
| for (const message of messages) { | |
| if (!message.parts) continue; | |
| for (const part of message.parts) { | |
| const p = part as any; | |
| if (p.type === 'tool-call' && p.toolCallId === toolCallId) { | |
| toolName = p.toolName; | |
| break; | |
| } else if ( | |
| p.type?.startsWith('tool-') && | |
| p.toolCallId === toolCallId | |
| ) { | |
| toolName = p.type.substring(5); // Remove 'tool-' prefix | |
| break; | |
| } else if ( | |
| p.type === 'dynamic-tool' && | |
| p.toolCallId === toolCallId | |
| ) { | |
| toolName = p.toolName; | |
| break; | |
| } | |
| } | |
| if (toolName !== 'unknown') break; |
components/chat.tsx
Outdated
| const p = part as any | ||
| if (p.type === 'tool-call' && p.toolCallId === toolCallId) { | ||
| toolName = p.toolName | ||
| break | ||
| } else if ( | ||
| p.type?.startsWith('tool-') && | ||
| p.toolCallId === toolCallId | ||
| ) { | ||
| toolName = p.type.substring(5) // Remove 'tool-' prefix | ||
| break | ||
| } else if ( | ||
| p.type === 'dynamic-tool' && | ||
| p.toolCallId === toolCallId | ||
| ) { | ||
| toolName = p.toolName |
There was a problem hiding this comment.
Using 'as any' type assertion bypasses type checking. Consider using proper type guards or defining specific interfaces for message parts.
| const p = part as any | |
| if (p.type === 'tool-call' && p.toolCallId === toolCallId) { | |
| toolName = p.toolName | |
| break | |
| } else if ( | |
| p.type?.startsWith('tool-') && | |
| p.toolCallId === toolCallId | |
| ) { | |
| toolName = p.type.substring(5) // Remove 'tool-' prefix | |
| break | |
| } else if ( | |
| p.type === 'dynamic-tool' && | |
| p.toolCallId === toolCallId | |
| ) { | |
| toolName = p.toolName | |
| if (isToolCallPart(part) && part.toolCallId === toolCallId) { | |
| toolName = part.toolName | |
| break | |
| } else if ( | |
| isToolTypePart(part) && | |
| part.toolCallId === toolCallId | |
| ) { | |
| toolName = part.type.substring(5) // Remove 'tool-' prefix | |
| break | |
| } else if ( | |
| isDynamicToolPart(part) && | |
| part.toolCallId === toolCallId | |
| ) { | |
| toolName = part.toolName |
- Add proper TypeScript types for dynamic tools (MCPClient, DynamicToolConfig, DynamicToolPart) - Replace all 'any' types with proper interfaces throughout codebase - Add type guards for tool parts (isDynamicToolPart, isToolCallPart, isToolTypePart) - Optimize tool name lookup in chat.tsx to break early when found - Cast partial objects to full types where TypeScript requires it - Improve overall type safety without using 'any' type assertions
feat: add support for AI SDK v5 dynamic tools
Summary
This PR adds support for AI SDK v5's dynamic tools feature, enabling runtime-defined tools beyond compile-time static tools.
Key Changes
dynamic-toolUI parts from AI SDK v5DynamicToolDisplaycomponent for rendering dynamic tool invocationsWhat are Dynamic Tools?
Dynamic tools are tools whose input/output types are not known at compile time, including:
Migration Required
After merging, run the database migration to update the schema:
Or manually apply:
Breaking Changes
regenerate-assistant-message→regenerate-message,submit-user-message→submit-messageaddToolResultnow requires atoolparameterTesting
All TypeScript, linting, and build checks pass. The implementation is compatible with existing static tools while adding support for dynamic ones.