diff --git a/package.json b/package.json index c953891..3aa61b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@netbirdio/explain", - "version": "0.1.5", + "version": "0.1.6", "description": "Full-stack AI assistant library with React frontend components and Node.js backend handler", "license": "BSD-3-Clause", "main": "./dist/client/index.js", diff --git a/src/server/handler.ts b/src/server/handler.ts index c184497..0e0623e 100644 --- a/src/server/handler.ts +++ b/src/server/handler.ts @@ -2,7 +2,7 @@ import type { Message } from "../types"; import { AnthropicProvider } from "./providers/anthropic"; import { DifyProvider } from "./providers/dify"; import { OpenAIProvider } from "./providers/openai"; -import type { LLMProvider, Middleware, Tool } from "./providers/types"; +import type { LLMProvider, McpServer, Middleware, Tool } from "./providers/types"; export type AssistantConfig = { provider: "anthropic" | "openai" | "dify" | LLMProvider; @@ -12,6 +12,7 @@ export type AssistantConfig = { systemPrompt?: string; middleware?: Middleware[]; tools?: Tool[]; + mcpServers?: McpServer[]; }; type HandlerOptions = { @@ -133,7 +134,7 @@ export function createAssistant(config: AssistantConfig) { systemPrompt = result.systemPrompt; } - const reply = await provider.chat(messages, systemPrompt, config.tools); + const reply = await provider.chat(messages, systemPrompt, config.tools, config.mcpServers); return { reply }; } diff --git a/src/server/index.ts b/src/server/index.ts index 57bb517..5ae145f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -3,4 +3,4 @@ export type { AssistantConfig } from "./handler"; export { AnthropicProvider } from "./providers/anthropic"; export { DifyProvider } from "./providers/dify"; export { OpenAIProvider } from "./providers/openai"; -export type { LLMProvider, ProviderConfig, Middleware, Tool } from "./providers/types"; \ No newline at end of file +export type { LLMProvider, McpServer, ProviderConfig, Middleware, Tool } from "./providers/types"; \ No newline at end of file diff --git a/src/server/providers/anthropic.ts b/src/server/providers/anthropic.ts index 46071cb..5c8fb53 100644 --- a/src/server/providers/anthropic.ts +++ b/src/server/providers/anthropic.ts @@ -1,5 +1,5 @@ import type { Message } from "../../types"; -import type { LLMProvider, ProviderConfig, Tool } from "./types"; +import type { LLMProvider, McpServer, ProviderConfig, Tool } from "./types"; type AnthropicMessage = { role: "user" | "assistant"; @@ -20,17 +20,36 @@ export class AnthropicProvider implements LLMProvider { this.model = config.model || "claude-sonnet-4-20250514"; } - async chat(messages: Message[], systemPrompt?: string, tools?: Tool[]): Promise { + async chat(messages: Message[], systemPrompt?: string, tools?: Tool[], mcpServers?: McpServer[]): Promise { const anthropicMessages: AnthropicMessage[] = messages.map((m) => ({ role: m.role === "context" || m.role === "system" ? ("user" as const) : m.role, content: m.role === "context" ? `[Context]: ${m.content}` : m.content, })); - const toolDefs = tools?.map((t) => ({ - name: t.name, - description: t.description, - input_schema: t.input_schema, - })); + const toolDefs: Record[] = []; + + if (tools?.length) { + for (const t of tools) { + toolDefs.push({ + name: t.name, + description: t.description, + input_schema: t.input_schema, + }); + } + } + + if (mcpServers?.length) { + for (const mcp of mcpServers) { + const mcpTool: Record = { + type: "mcp", + server_label: mcp.server_label, + server_url: mcp.server_url, + }; + if (mcp.headers) mcpTool.headers = mcp.headers; + if (mcp.allowed_tools) mcpTool.allowed_tools = mcp.allowed_tools; + toolDefs.push(mcpTool); + } + } const toolsByName = new Map(tools?.map((t) => [t.name, t])); @@ -52,13 +71,19 @@ export class AnthropicProvider implements LLMProvider { body.tools = toolDefs; } + const headers: Record = { + "Content-Type": "application/json", + "x-api-key": this.apiKey, + "anthropic-version": "2023-06-01", + }; + + if (mcpServers?.length) { + headers["anthropic-beta"] = "mcp-client-2025-04-04"; + } + const response = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", - headers: { - "Content-Type": "application/json", - "x-api-key": this.apiKey, - "anthropic-version": "2023-06-01", - }, + headers, body: JSON.stringify(body), }); @@ -76,6 +101,12 @@ export class AnthropicProvider implements LLMProvider { lastTextResponse = textBlock.text; } + // MCP server-side tools hit iteration limit — re-send to continue + if (data.stop_reason === "pause_turn") { + anthropicMessages.push({ role: "assistant", content: data.content }); + continue; + } + if (data.stop_reason !== "tool_use") { if (!lastTextResponse) { throw new Error("No text content in Anthropic response"); @@ -83,7 +114,7 @@ export class AnthropicProvider implements LLMProvider { return lastTextResponse; } - // Handle tool calls + // Handle client-side tool calls const toolUseBlocks = data.content.filter( (block: { type: string }) => block.type === "tool_use", ); diff --git a/src/server/providers/types.ts b/src/server/providers/types.ts index 7d5e39c..0c01a6f 100644 --- a/src/server/providers/types.ts +++ b/src/server/providers/types.ts @@ -7,8 +7,15 @@ export type Tool = { execute: (input: Record) => Promise; }; +export type McpServer = { + server_label: string; + server_url: string; + headers?: Record; + allowed_tools?: string[]; +}; + export interface LLMProvider { - chat(messages: Message[], systemPrompt?: string, tools?: Tool[]): Promise; + chat(messages: Message[], systemPrompt?: string, tools?: Tool[], mcpServers?: McpServer[]): Promise; } export type ProviderConfig = {