Truly Useful Anti-Patterns: Client-Tool-Execution MCP Server
Client-Tool-Execution MCP Server 🚀
Truly Useful Anti-Patterns: Client-Tool-Execution MCP Server
Core Features 🎯
- Dynamic Tool Registration: Clients connect and register their tools automatically
- Client-Side Tool Execution: Tools execute on the client side, not the server - perfect for browser DOM manipulation, local file access, or environment-specific operations
- Transparent Proxy: Server acts as a proxy, routing tool calls to the appropriate client for execution
This enables you to:
- 🔄 Register custom tools dynamically when clients connect
- ⚡ Execute tools directly on the client - not on the server
- 🌐 Build tools that interact with client-specific environments (browser DOM, local files, etc.)
- 🔗 Create flexible, client-driven AI tool ecosystems where execution happens where the data lives
Getting Started 🚀
Installation
# Using Deno (recommended) import { createClientExecServer, createClientExecClient } from "jsr:@mcpc/cmcp"; # Or add to your deno.json { "imports": { "@mcpc/cmcp": "jsr:@mcpc/cmcp@^0.0.2" } }
Basic Setup
- Install the package: Add
@mcpc/cmcp
to your project - Start the server demo:
deno run --allow-net --allow-read --allow-env server.ts
- Run the client demo:
deno run --allow-net client.ts
Complete Example
Here's a minimal working example:
Server Usage 📡
The server acts as a proxy and registry - it has no predefined tools and simply routes execution to clients:
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { createClientExecServer } from "@mcpc/cmcp"; // Server is just a proxy - no tools, no execution logic const server = createClientExecServer( new Server({ name: "dynamic-mcp-server", version: "1.0.0" }), "dynamic-server", ); // Server routes all tool calls to the appropriate client // All execution happens on the client side
Client Usage 🖥️
Clients register tools with implementations that execute locally on the client:
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { type ClientToolDefinition, createClientExecClient } from "@mcpc/cmcp"; const client = createClientExecClient( new Client({ name: "browser-client", version: "1.0.0" }), "browser-client-001", ); // Define tools with LOCAL implementations (executed on client) const tools: ClientToolDefinition[] = [ { name: "querySelector", description: "Query DOM elements using CSS selectors", inputSchema: { type: "object", properties: { selector: { type: "string", description: "CSS selector to query" }, action: { type: "string", description: "Action to perform", enum: ["getText", "click", "getAttribute"], }, attribute: { type: "string", description: "Attribute name" }, }, required: ["selector", "action"], }, // 🔥 Implementation runs on CLIENT side - has access to DOM, local files, etc. implementation: async (args: Record<string, unknown>) => { const { selector, action, attribute } = args; const element = document.querySelector(selector as string); if (!element) { throw new Error(`Element not found: ${selector}`); } switch (action) { case "getText": return element.textContent || ""; case "click": element.click(); return `Clicked element: ${selector}`; case "getAttribute": return element.getAttribute(attribute as string); default: throw new Error(`Unknown action: ${action}`); } }, }, ]; // Register tools (stored locally until connection) client.registerTools(tools); // Connect and register tools to server await client.connect( new SSEClientTransport(new URL("http://localhost:9000/sse")), ); console.log("Client connected and tools registered!"); // Client stays connected to handle tool execution requests
Example Tool Call 🔧
// External MCP client connecting to the server import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; const mcpClient = new Client({ name: "external-client", version: "1.0.0", }); await mcpClient.connect( new SSEClientTransport(new URL("http://localhost:9000/sse")), ); // Call tools registered by connected clients const result = await mcpClient.callTool({ name: "querySelector", arguments: { selector: "#my-button", action: "click", }, }); console.log(result); // "Clicked element: #my-button" // ✨ The actual DOM manipulation happened on the CLIENT side!
Why Client-Side Execution? 🤔
Traditional MCP: Tools execute on the server
- ❌ Server needs access to all resources (files, DOM, APIs)
- ❌ Security concerns with server-side execution
- ❌ Limited to server environment capabilities
Client-Tool-Execution MCP: Tools execute on the client
- ✅ Client has natural access to its own environment (DOM, local files, etc.)
- ✅ Better security - no need to expose sensitive resources to server
- ✅ Scalable - each client handles its own execution load
- ✅ Environment-specific - browser clients can manipulate DOM, desktop clients can access files
Architecture Flow 🔄
- Server: Starts as an empty proxy with no predefined tools
- Client Connect: Client establishes SSE connection to server
- Tool Registration: Client sends tool definitions (schema only) via
client/register_tools
- Server Registry: Server updates its tool registry with client's tool schemas
- MCP Call: External system discovers and calls tools via server
- Proxy Call: Server proxies call to appropriate client via notification
- Client Execution: 🔥 Tool runs on CLIENT side with full access to client environment
- Response: Result flows back through server to caller
- Client Disconnect: Server automatically removes client's tools
Key Point: The server never executes tools - it only routes calls to clients where the actual execution happens!
Add Package
deno add jsr:@mcpc/cmcp
Import symbol
import * as cmcp from "@mcpc/cmcp";
Import directly with a jsr specifier
import * as cmcp from "jsr:@mcpc/cmcp";
Add Package
pnpm i jsr:@mcpc/cmcp
pnpm dlx jsr add @mcpc/cmcp
Import symbol
import * as cmcp from "@mcpc/cmcp";
Add Package
yarn add jsr:@mcpc/cmcp
yarn dlx jsr add @mcpc/cmcp
Import symbol
import * as cmcp from "@mcpc/cmcp";
Add Package
vlt install jsr:@mcpc/cmcp
Import symbol
import * as cmcp from "@mcpc/cmcp";
Add Package
npx jsr add @mcpc/cmcp
Import symbol
import * as cmcp from "@mcpc/cmcp";
Add Package
bunx jsr add @mcpc/cmcp
Import symbol
import * as cmcp from "@mcpc/cmcp";