Skip to content

Commit 2e5d56d

Browse files
0xbbjokerclaude
andauthored
feat!: Dynamic MCP tool actions (v1.8.0) (#22)
* feat: add StreamableHTTP transport support and code quality improvements - Add StreamableHTTPClientTransport for non-SSE HTTP connections - Add headers support for HTTP transports (auth injection) - Simplify error handling with errMsg helper - Clean up verbose logging and redundant code - Fix restartConnection to throw on missing connection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat!: dynamic MCP tool actions with schema caching BREAKING CHANGE: MCP tools are now registered as native ElizaOS actions instead of using a single CALL_MCP_TOOL action. This reduces LLM calls from 3 to 1 per tool invocation. - **Dynamic Action Registration**: Each MCP tool becomes a native ElizaOS action (e.g., GOOGLE_SEARCH, GITHUB_GET_FILE) - **Schema Caching**: Optional Redis/Upstash caching for tool schemas enables lazy connections - **Tool Compatibility Layer**: Automatic schema transformation for different LLM providers (OpenAI, Anthropic, Google) - **Parameter Validation**: Validates required parameters before calling MCP server - Removed `CALL_MCP_TOOL` action and related templates - Added `dynamic-tool-actions.ts` for action factory - Added `schema-converter.ts` for JSON Schema to ActionParameter conversion - Added `action-naming.ts` for collision-free action name generation - Added `cache/schema-cache.ts` for optional Redis caching - Comprehensive test suite (204 tests, 388 assertions) - Removed dead code and over-engineering - Fixed silent error swallowing - Fixed ESM compatibility issues Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 55bc62a commit 2e5d56d

39 files changed

Lines changed: 3911 additions & 2756 deletions

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@elizaos/plugin-mcp",
33
"description": "ElizaOS plugin to integrate with MCP (Model Context Protocol) servers",
4-
"version": "1.7.1",
4+
"version": "1.8.0",
55
"type": "module",
66
"main": "dist/index.js",
77
"module": "dist/index.js",
@@ -41,6 +41,8 @@
4141
"elizaos-plugins"
4242
],
4343
"scripts": {
44+
"test": "bun test tests/",
45+
"test:watch": "bun test --watch tests/",
4446
"build": "bun run build.ts",
4547
"dev": "bun --hot build.ts",
4648
"clean": "rm -rf dist .turbo node_modules .turbo-tsconfig.json tsconfig.tsbuildinfo",

src/actions/callToolAction.ts

Lines changed: 0 additions & 198 deletions
This file was deleted.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import {
2+
type Action,
3+
type ActionResult,
4+
type HandlerCallback,
5+
type IAgentRuntime,
6+
type Memory,
7+
type State,
8+
logger,
9+
} from "@elizaos/core";
10+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
11+
import type { McpService } from "../service";
12+
import { MCP_SERVICE_NAME } from "../types";
13+
import { type ActionParameter, convertJsonSchemaToActionParams, validateParamsAgainstSchema } from "../utils/schema-converter";
14+
import { toActionName, generateSimiles, makeUniqueActionName } from "../utils/action-naming";
15+
import { processToolResult } from "../utils/processing";
16+
17+
export interface McpToolAction extends Action {
18+
parameters?: Record<string, ActionParameter>;
19+
_mcpMeta: { serverName: string; toolName: string; originalSchema: Tool["inputSchema"] };
20+
}
21+
22+
function extractParams(message: Memory, state?: State): Record<string, unknown> {
23+
const content = message.content as Record<string, unknown>;
24+
return (content.actionParams as Record<string, unknown>) ||
25+
(content.actionInput as Record<string, unknown>) ||
26+
(state?.data?.actionParams as Record<string, unknown>) ||
27+
{};
28+
}
29+
30+
export function createMcpToolAction(serverName: string, tool: Tool, existingNames: Set<string>): McpToolAction {
31+
const actionName = makeUniqueActionName(serverName, tool.name, existingNames);
32+
const description = `${tool.description || `Execute ${tool.name}`} (MCP: ${serverName}/${tool.name})`;
33+
34+
return {
35+
name: actionName,
36+
description,
37+
similes: generateSimiles(serverName, tool.name),
38+
parameters: convertJsonSchemaToActionParams(tool.inputSchema),
39+
40+
validate: async (runtime: IAgentRuntime) => {
41+
const svc = runtime.getService<McpService>(MCP_SERVICE_NAME);
42+
if (!svc) return false;
43+
if (svc.isLazyConnection(serverName)) return true;
44+
45+
const server = svc.getServers().find(s => s.name === serverName);
46+
return server?.status === "connected" && !!server.tools?.some(t => t.name === tool.name);
47+
},
48+
49+
handler: async (runtime, message, state, _options, callback): Promise<ActionResult> => {
50+
const svc = runtime.getService<McpService>(MCP_SERVICE_NAME);
51+
if (!svc) {
52+
return { success: false, error: "MCP service not available", data: { actionName, serverName, toolName: tool.name } };
53+
}
54+
55+
const params = extractParams(message, state);
56+
logger.info({ serverName, toolName: tool.name, params }, `[MCP] Executing ${actionName}`);
57+
58+
const errors = validateParamsAgainstSchema(params, tool.inputSchema);
59+
const missing = errors.filter(e => e.startsWith("Missing required"));
60+
if (missing.length > 0) {
61+
logger.error({ missing, params }, `[MCP] Missing required params for ${actionName}`);
62+
return { success: false, error: missing.join(", "), data: { actionName, serverName, toolName: tool.name } };
63+
}
64+
65+
const warnings = errors.filter(e => !e.startsWith("Missing required"));
66+
if (warnings.length > 0) {
67+
logger.warn({ warnings, params }, `[MCP] Type warnings for ${actionName}`);
68+
}
69+
70+
const result = await svc.callTool(serverName, tool.name, params);
71+
const { toolOutput, hasAttachments, attachments } = processToolResult(result, serverName, tool.name, runtime, message.entityId);
72+
73+
if (result.isError) {
74+
logger.error({ serverName, toolName: tool.name, output: toolOutput }, `[MCP] Tool error`);
75+
return {
76+
success: false,
77+
error: toolOutput || "Tool execution failed",
78+
text: toolOutput,
79+
data: { actionName, serverName, toolName: tool.name, toolArguments: params, isError: true },
80+
};
81+
}
82+
83+
if (callback && hasAttachments && attachments.length > 0) {
84+
await callback({ text: `Executed ${serverName}/${tool.name}`, attachments });
85+
}
86+
87+
return {
88+
success: true,
89+
text: toolOutput,
90+
values: { success: true, serverName, toolName: tool.name, hasAttachments, output: toolOutput },
91+
data: { actionName, serverName, toolName: tool.name, toolArguments: params, output: toolOutput, attachments: attachments.length > 0 ? attachments : undefined },
92+
};
93+
},
94+
95+
examples: [[
96+
{ name: "{{user}}", content: { text: `Can you use ${tool.name}?` } },
97+
{ name: "{{assistant}}", content: { text: `I'll execute ${tool.name} for you.`, actions: [actionName] } },
98+
]],
99+
100+
_mcpMeta: { serverName, toolName: tool.name, originalSchema: tool.inputSchema },
101+
};
102+
}
103+
104+
export function createMcpToolActions(serverName: string, tools: Tool[], existingNames: Set<string>): McpToolAction[] {
105+
const actions = tools.map(tool => {
106+
const action = createMcpToolAction(serverName, tool, existingNames);
107+
existingNames.add(action.name);
108+
logger.debug({ actionName: action.name, serverName, toolName: tool.name }, `[MCP] Created action`);
109+
return action;
110+
});
111+
112+
logger.info({ serverName, toolCount: actions.length }, `[MCP] Created ${actions.length} actions for ${serverName}`);
113+
return actions;
114+
}
115+
116+
export function isMcpToolAction(action: Action): action is McpToolAction {
117+
return "_mcpMeta" in action && typeof (action as McpToolAction)._mcpMeta === "object";
118+
}
119+
120+
export function getMcpToolActionsForServer(actions: Action[], serverName: string): McpToolAction[] {
121+
return actions.filter((a): a is McpToolAction => isMcpToolAction(a) && a._mcpMeta.serverName === serverName);
122+
}

src/cache/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* MCP Cache Module
3+
*/
4+
export { McpSchemaCache, getSchemaCache } from "./schema-cache";

0 commit comments

Comments
 (0)