-
Notifications
You must be signed in to change notification settings - Fork 0
feat: MCP Tool Chain Support #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -312,6 +312,39 @@ const realtime = new OpenAIRealtimeProvider({ | |||||
| realtime.registerFunction(weatherTool); | ||||||
| ``` | ||||||
|
|
||||||
| ### MCP Servers (Model Context Protocol) | ||||||
|
|
||||||
| You can mount any number of MCP servers and expose their tools directly to OpenAI. Each server describes its tools via the MCP spec; the provider handles the rest (connections, pagination, name-spacing, execution, and returning results to the realtime session). | ||||||
|
|
||||||
| ```tsx | ||||||
| const realtime = new OpenAIRealtimeProvider({ | ||||||
| apiKey: process.env.OPENAI_API_KEY || "", | ||||||
| mcpServers: [ | ||||||
| { | ||||||
| id: "docs", | ||||||
| url: process.env.DOCS_MCP_URL!, | ||||||
| headers: { | ||||||
| Authorization: `Bearer ${process.env.DOCS_MCP_TOKEN!}`, | ||||||
| }, | ||||||
| toolNamePrefix: "docs__", // optional (defaults to `${id}__`) | ||||||
| }, | ||||||
| { | ||||||
| id: "inventory", | ||||||
| url: process.env.INVENTORY_MCP_URL!, | ||||||
| }, | ||||||
| ], | ||||||
| }); | ||||||
| ``` | ||||||
|
|
||||||
| `MCPServerConfig` options: | ||||||
|
|
||||||
| - `id`: unique identifier. Also used as the default prefix for tools (`${id}__toolName`). | ||||||
| - `url`: Streamable HTTP endpoint for the server (e.g., `https://.../mcp`). | ||||||
| - `headers`: optional HTTP headers for auth. | ||||||
| - `toolNamePrefix`: override the default prefix (set to `null` to keep original names). | ||||||
|
||||||
| - `toolNamePrefix`: override the default prefix (set to `null` to keep original names). | |
| - `toolNamePrefix`: override the default prefix (set to `null` to keep original names). **Warning:** when set to `null`, tool names are not namespaced, so tools from different MCP servers or manual tools can collide; in case of a collision, the last registered tool overwrites earlier ones. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,7 @@ import { | |||||||||||||||||||||||||||||||||||||||||||
| } from "@khaveeai/core"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { v4 as uuidv4 } from "uuid"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { ToolExecutor } from "./ToolExecutor"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { MCPToolManager } from "./mcp/MCPToolManager"; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export class OpenAIRealtimeProvider implements RealtimeProvider { | ||||||||||||||||||||||||||||||||||||||||||||
| private config: RealtimeConfig; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -20,6 +21,10 @@ export class OpenAIRealtimeProvider implements RealtimeProvider { | |||||||||||||||||||||||||||||||||||||||||||
| private audioContext: AudioContext | null = null; | ||||||||||||||||||||||||||||||||||||||||||||
| private audioStream: MediaStream | null = null; | ||||||||||||||||||||||||||||||||||||||||||||
| private toolExecutor: ToolExecutor; | ||||||||||||||||||||||||||||||||||||||||||||
| private manualTools: RealtimeTool[] = []; | ||||||||||||||||||||||||||||||||||||||||||||
| private mcpTools: RealtimeTool[] = []; | ||||||||||||||||||||||||||||||||||||||||||||
| private mcpManager?: MCPToolManager; | ||||||||||||||||||||||||||||||||||||||||||||
| private mcpInitialization?: Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // State | ||||||||||||||||||||||||||||||||||||||||||||
| public isConnected = false; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -64,9 +69,20 @@ export class OpenAIRealtimeProvider implements RealtimeProvider { | |||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| this.toolExecutor = new ToolExecutor(); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // Register initial tools | ||||||||||||||||||||||||||||||||||||||||||||
| if (this.config.tools) { | ||||||||||||||||||||||||||||||||||||||||||||
| this.config.tools.forEach((tool) => this.registerFunction(tool)); | ||||||||||||||||||||||||||||||||||||||||||||
| this.config.tools.forEach((tool) => this.registerTool(tool, "manual")); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if (this.config.mcpServers && this.config.mcpServers.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||
| this.mcpManager = new MCPToolManager(this.config.mcpServers); | ||||||||||||||||||||||||||||||||||||||||||||
| this.mcpInitialization = this.mcpManager | ||||||||||||||||||||||||||||||||||||||||||||
| .initialize() | ||||||||||||||||||||||||||||||||||||||||||||
| .then((tools) => { | ||||||||||||||||||||||||||||||||||||||||||||
| tools.forEach((tool) => this.registerTool(tool, "mcp")); | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| .catch((error) => { | ||||||||||||||||||||||||||||||||||||||||||||
| console.error("Failed to initialize MCP servers:", error); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+76
to
86
|
||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -98,7 +114,7 @@ export class OpenAIRealtimeProvider implements RealtimeProvider { | |||||||||||||||||||||||||||||||||||||||||||
| this.dataChannel = dataChannel; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| dataChannel.onopen = () => { | ||||||||||||||||||||||||||||||||||||||||||||
| this.configureSession(); | ||||||||||||||||||||||||||||||||||||||||||||
| void this.configureSession(); | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| dataChannel.onmessage = (event) => { | ||||||||||||||||||||||||||||||||||||||||||||
| this.handleDataChannelMessage(event); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -247,15 +263,43 @@ export class OpenAIRealtimeProvider implements RealtimeProvider { | |||||||||||||||||||||||||||||||||||||||||||
| * Register a function/tool | ||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
| registerFunction(tool: RealtimeTool): void { | ||||||||||||||||||||||||||||||||||||||||||||
| this.registerTool(tool, "manual"); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
265
to
+267
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| private registerTool(tool: RealtimeTool, origin: "manual" | "mcp"): void { | ||||||||||||||||||||||||||||||||||||||||||||
| this.toolExecutor.register(tool.name, tool.execute); | ||||||||||||||||||||||||||||||||||||||||||||
| const target = origin === "manual" ? this.manualTools : this.mcpTools; | ||||||||||||||||||||||||||||||||||||||||||||
| const existingIndex = target.findIndex((existing) => existing.name === tool.name); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if (existingIndex >= 0) { | ||||||||||||||||||||||||||||||||||||||||||||
| target[existingIndex] = tool; | ||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||
| target.push(tool); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| private getAllRegisteredTools(): RealtimeTool[] { | ||||||||||||||||||||||||||||||||||||||||||||
| const combined = new Map<string, RealtimeTool>(); | ||||||||||||||||||||||||||||||||||||||||||||
| [...this.manualTools, ...this.mcpTools].forEach((tool) => { | ||||||||||||||||||||||||||||||||||||||||||||
| combined.set(tool.name, tool); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+283
to
+285
|
||||||||||||||||||||||||||||||||||||||||||||
| [...this.manualTools, ...this.mcpTools].forEach((tool) => { | |
| combined.set(tool.name, tool); | |
| }); | |
| // First, register all manual tools. | |
| this.manualTools.forEach((tool) => { | |
| combined.set(tool.name, tool); | |
| }); | |
| // Then, register MCP tools, warning on name collisions and | |
| // preserving the existing behavior where MCP tools take precedence. | |
| this.mcpTools.forEach((tool) => { | |
| if (combined.has(tool.name)) { | |
| // Name collision: MCP tool overrides a manual tool with the same name. | |
| console.warn( | |
| `[OpenAIRealtimeProvider] Tool name collision detected for "${tool.name}". ` + | |
| "The MCP tool will override the previously registered manual tool." | |
| ); | |
| } | |
| combined.set(tool.name, tool); | |
| }); |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The safeguard 'tool.parameters || {}' is good, but this change suggests that tool.parameters might be undefined. However, the RealtimeTool type definition requires parameters to always be present (not optional). If parameters can indeed be undefined in practice, the type definition should be updated to reflect this, otherwise this null check is unnecessary.
| Object.entries(tool.parameters || {}).forEach( | |
| Object.entries(tool.parameters).forEach( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example encourages putting
NEXT_PUBLIC_OPENAI_API_KEYandNEXT_PUBLIC_DOCS_MCP_TOKENintoNEXT_PUBLIC_*environment variables, which are bundled into client-side JavaScript and fully exposed to any user of the app. An attacker can trivially extract these values from the browser, then reuse your OpenAI API key and MCP token to call your backends, exhaust quotas, or access data. To avoid leaking these secrets, keep them in server-only configuration (non-public env vars or a backend proxy) and do not expose them to untrusted clients.