index.ts•11.1 kB
#!/usr/bin/env node
// UTCP-MCP Bridge Entry Point
// This is the main entry point for the npx @utcp/mcp-bridge command
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import path from "path";
import { promises as fs } from "fs";
import { parse as parseDotEnv } from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import "@utcp/http";
import "@utcp/text";
import "@utcp/mcp";
import "@utcp/cli";
import "@utcp/dotenv-loader"
import "@utcp/file"
import {
UtcpClient,
CallTemplateSchema,
InMemConcurrentToolRepository,
TagSearchStrategy,
DefaultVariableSubstitutor,
ensureCorePluginsInitialized,
UtcpClientConfigSerializer
} from "@utcp/sdk";
import type { UtcpClientConfig } from "@utcp/sdk";
import { CodeModeUtcpClient } from "@utcp/code-mode";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
ensureCorePluginsInitialized();
let utcpClient: CodeModeUtcpClient | null = null;
async function main() {
setupMcpTools();
utcpClient = await initializeUtcpClient();
const transport = new StdioServerTransport();
await mcp.connect(transport);
}
const mcp = new McpServer({
name: "CodeMode-MCP",
version: "1.0.0",
});
function setupMcpTools() {
// Register MCP prompt for using the code mode server
mcp.registerPrompt("utcp_codemode_usage", {
title: "UTCP Code Mode Usage Guide",
description: "Comprehensive guide on how to use the UTCP Code Mode MCP server for executing TypeScript code with tool access."
}, async () => {
const codeInstructions = `# UTCP Code Mode MCP Server Usage Guide
You have access to a powerful UTCP Code Mode MCP server that allows you to execute TypeScript code with direct access to registered tools.
## Workflow: Always Follow This Pattern
### 1. 🔍 DISCOVER TOOLS FIRST
**Always start by searching for relevant tools before writing code:**
- Use \`search_tools\` with a description of your task to find relevant tools
- This returns tools with their TypeScript interfaces - study these carefully
- Use \`tool_info\` to get detailed interface information for specific tools if needed
${CodeModeUtcpClient.AGENT_PROMPT_TEMPLATE}
- in the call_tool_chain code, return the result that you want to see, your code will be wrapped in an async function and executed
Remember: The power of this system comes from combining multiple tools in sophisticated TypeScript code execution workflows.`;
return {
messages: [{
role: "user",
content: {
type: "text",
text: codeInstructions
}
}]
};
});
mcp.registerTool("register_manual", {
title: "Register a UTCP Manual",
description: "Registers a new tool provider by providing its call template.",
inputSchema: { manual_call_template: CallTemplateSchema.describe("The call template for the UTCP Manual endpoint.") },
}, async (input) => {
const client = await initializeUtcpClient();
try {
const result = await client.registerManual(input.manual_call_template as any);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (e: any) {
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }] };
}
});
mcp.registerTool("deregister_manual", {
title: "Deregister a UTCP Manual",
description: "Deregisters a tool provider from the UTCP client.",
inputSchema: { manual_name: z.string().describe("The name of the manual to deregister.") },
}, async (input) => {
const client = await initializeUtcpClient();
try {
const success = await client.deregisterManual(input.manual_name);
const message = success ? `Manual '${input.manual_name}' deregistered.` : `Manual '${input.manual_name}' not found.`;
return { content: [{ type: "text", text: JSON.stringify({ success, message }) }] };
} catch (e: any) {
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }] };
}
});
mcp.registerTool("search_tools", {
title: "Search for UTCP Tools",
description: "Searches for relevant tools based on a task description.",
inputSchema: {
task_description: z.string().describe("A natural language description of the task."),
limit: z.number().optional().default(10),
},
}, async (input) => {
const client = await initializeUtcpClient();
try {
const tools = await client.searchTools(input.task_description, input.limit);
const toolsWithInterfaces = tools.map(t => ({
name: t.name,
description: t.description,
typescript_interface: client.toolToTypeScriptInterface(t)
}));
return { content: [{ type: "text", text: JSON.stringify({ tools: toolsWithInterfaces }) }] };
} catch (e: any) {
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }] };
}
});
mcp.registerTool("list_tools", {
title: "List All Registered UTCP Tools",
description: "Returns a list of all tool names currently registered.",
inputSchema: {},
}, async () => {
const client = await initializeUtcpClient();
try {
const tools = await client.config.tool_repository.getTools();
const toolNames = tools.map(t => t.name);
return { content: [{ type: "text", text: JSON.stringify({ tools: toolNames }) }] };
} catch (e: any) {
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }] };
}
});
mcp.registerTool("get_required_keys_for_tool", {
title: "Get Required Variables for Tool",
description: "Get required environment variables for a registered tool.",
inputSchema: {
tool_name: z.string().describe("Name of the tool to get required variables for."),
},
}, async (input) => {
const client = await initializeUtcpClient();
try {
const variables = await client.getRequiredVariablesForRegisteredTool(input.tool_name);
return { content: [{ type: "text", text: JSON.stringify({ success: true, tool_name: input.tool_name, required_variables: variables }) }] };
} catch (e: any) {
return { content: [{ type: "text", text: JSON.stringify({ success: false, tool_name: input.tool_name, error: e.message }) }] };
}
});
mcp.registerTool("tool_info", {
title: "Get Tool Information with TypeScript Interface",
description: "Get complete information about a specific tool including TypeScript interface definition.",
inputSchema: {
tool_name: z.string().describe("Name of the tool to get complete information for."),
},
}, async (input) => {
const client = await initializeUtcpClient();
try {
const tool = await client.config.tool_repository.getTool(input.tool_name);
if (!tool) {
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Tool '${input.tool_name}' not found` }) }] };
}
const typescript_interface = client.toolToTypeScriptInterface(tool);
return { content: [{ type: "text", text: typescript_interface }] };
} catch (e: any) {
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }] };
}
});
// Code Mode specific tools
mcp.registerTool("call_tool_chain", {
title: "Execute TypeScript Code with Tool Access",
description: "Execute TypeScript code with direct access to all registered tools as hierarchical functions (e.g., manual.tool()).",
inputSchema: {
code: z.string().describe("TypeScript code to execute with access to all registered tools."),
timeout: z.number().optional().default(30000).describe("Optional timeout in milliseconds (default: 30000)."),
max_output_size: z.number().optional().default(200000).describe("Optional maximum output size in characters (default: 200000)."),
},
}, async (input) => {
const client = await initializeUtcpClient();
try {
const { result, logs } = await client.callToolChain(input.code, input.timeout);
const content = JSON.stringify({ success: true, result, logs })
if (content.length > input.max_output_size) {
return { content: [{ type: "text", text: content.slice(0, input.max_output_size) + "...\nmax_output_size exceeded" }] };
}
return { content: [{ type: "text", text: content }] };
} catch (e: any) {
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }] };
}
});
}
async function initializeUtcpClient(): Promise<CodeModeUtcpClient> {
if (utcpClient) {
return utcpClient;
}
// Look for config file: 1) Environment variable, 2) Current working directory, 3) Package directory
const cwd = process.cwd();
const packageDir = __dirname;
let configPath: string;
let scriptDir: string;
// Check if UTCP_CONFIG_FILE environment variable is set
if (process.env.UTCP_CONFIG_FILE) {
configPath = path.resolve(process.env.UTCP_CONFIG_FILE);
scriptDir = path.dirname(configPath);
try {
await fs.access(configPath);
} catch {
console.warn(`UTCP config file specified in UTCP_CONFIG_FILE not found: ${configPath}`);
}
} else {
// Fall back to current working directory first, then package directory
configPath = path.resolve(cwd, '.utcp_config.json');
scriptDir = cwd;
try {
await fs.access(configPath);
} catch {
configPath = path.resolve(packageDir, '.utcp_config.json');
scriptDir = packageDir;
}
}
let rawConfig: any = {};
try {
const configFileContent = await fs.readFile(configPath, 'utf-8');
rawConfig = JSON.parse(configFileContent);
} catch (e: any) {
if (e.code !== 'ENOENT') {
console.warn(`Could not read or parse .utcp_config.json. Error: ${e.message}`);
}
}
const clientConfig = new UtcpClientConfigSerializer().validateDict(rawConfig);
const newClient = await CodeModeUtcpClient.create(scriptDir, clientConfig);
utcpClient = newClient;
return utcpClient;
}
main().catch(err => {
console.error("Failed to start UTCP-MCP Bridge:", err);
process.exit(1);
});