Terminal-based AI coding agent with interactive TUI for autonomous code generation.
Features: - Interactive TUI with React/Ink - Autonomous agent with tool calls (bash, read, write, edit, glob, grep) - Permission system with pattern-based rules - Session management with auto-compaction - Dual providers: GitHub Copilot and Ollama - MCP server integration - Todo panel and theme system - Streaming responses - GitHub-compatible project context
This commit is contained in:
373
src/services/mcp/client.ts
Normal file
373
src/services/mcp/client.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* MCP Client - Manages connection to a single MCP server
|
||||
*/
|
||||
|
||||
import { spawn, type ChildProcess } from "child_process";
|
||||
import type {
|
||||
MCPServerConfig,
|
||||
MCPServerInstance,
|
||||
MCPToolDefinition,
|
||||
MCPResourceDefinition,
|
||||
MCPToolCallResult,
|
||||
MCPConnectionState,
|
||||
} from "@/types/mcp";
|
||||
|
||||
/**
|
||||
* JSON-RPC message types
|
||||
*/
|
||||
interface JsonRpcRequest {
|
||||
jsonrpc: "2.0";
|
||||
id: number;
|
||||
method: string;
|
||||
params?: unknown;
|
||||
}
|
||||
|
||||
interface JsonRpcResponse {
|
||||
jsonrpc: "2.0";
|
||||
id: number;
|
||||
result?: unknown;
|
||||
error?: {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP Client class for managing a single server connection
|
||||
*/
|
||||
export class MCPClient {
|
||||
private config: MCPServerConfig;
|
||||
private process: ChildProcess | null = null;
|
||||
private state: MCPConnectionState = "disconnected";
|
||||
private tools: MCPToolDefinition[] = [];
|
||||
private resources: MCPResourceDefinition[] = [];
|
||||
private error: string | undefined;
|
||||
private requestId = 0;
|
||||
private pendingRequests: Map<
|
||||
number,
|
||||
{
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
> = new Map();
|
||||
private buffer = "";
|
||||
|
||||
constructor(config: MCPServerConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server instance state
|
||||
*/
|
||||
getInstance(): MCPServerInstance {
|
||||
return {
|
||||
config: this.config,
|
||||
state: this.state,
|
||||
tools: this.tools,
|
||||
resources: this.resources,
|
||||
error: this.error,
|
||||
pid: this.process?.pid,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the MCP server
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.state === "connected" || this.state === "connecting") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = "connecting";
|
||||
this.error = undefined;
|
||||
|
||||
try {
|
||||
if (this.config.transport === "stdio" || !this.config.transport) {
|
||||
await this.connectStdio();
|
||||
} else {
|
||||
throw new Error(
|
||||
`Transport type '${this.config.transport}' not yet supported`,
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize the connection
|
||||
await this.initialize();
|
||||
|
||||
// Discover tools and resources
|
||||
await this.discoverCapabilities();
|
||||
|
||||
this.state = "connected";
|
||||
} catch (err) {
|
||||
this.state = "error";
|
||||
this.error = err instanceof Error ? err.message : String(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect via stdio transport
|
||||
*/
|
||||
private async connectStdio(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const env = {
|
||||
...process.env,
|
||||
...this.config.env,
|
||||
};
|
||||
|
||||
this.process = spawn(this.config.command, this.config.args || [], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env,
|
||||
});
|
||||
|
||||
if (!this.process.stdout || !this.process.stdin) {
|
||||
reject(new Error("Failed to create stdio pipes"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.process.stdout.on("data", (data: Buffer) => {
|
||||
this.handleData(data.toString());
|
||||
});
|
||||
|
||||
this.process.stderr?.on("data", (data: Buffer) => {
|
||||
console.error(`[MCP ${this.config.name}] stderr:`, data.toString());
|
||||
});
|
||||
|
||||
this.process.on("error", (err) => {
|
||||
this.state = "error";
|
||||
this.error = err.message;
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.process.on("exit", (code) => {
|
||||
this.state = "disconnected";
|
||||
if (code !== 0) {
|
||||
this.error = `Process exited with code ${code}`;
|
||||
}
|
||||
});
|
||||
|
||||
// Give the process a moment to start
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming data from the server
|
||||
*/
|
||||
private handleData(data: string): void {
|
||||
this.buffer += data;
|
||||
|
||||
// Process complete JSON-RPC messages (newline-delimited)
|
||||
const lines = this.buffer.split("\n");
|
||||
this.buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const message = JSON.parse(line) as JsonRpcResponse;
|
||||
this.handleMessage(message);
|
||||
} catch {
|
||||
// Ignore parse errors for incomplete messages
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a JSON-RPC message
|
||||
*/
|
||||
private handleMessage(message: JsonRpcResponse): void {
|
||||
const pending = this.pendingRequests.get(message.id);
|
||||
if (pending) {
|
||||
this.pendingRequests.delete(message.id);
|
||||
if (message.error) {
|
||||
pending.reject(new Error(message.error.message));
|
||||
} else {
|
||||
pending.resolve(message.result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a JSON-RPC request
|
||||
*/
|
||||
private async sendRequest(
|
||||
method: string,
|
||||
params?: unknown,
|
||||
): Promise<unknown> {
|
||||
if (!this.process?.stdin) {
|
||||
throw new Error("Not connected");
|
||||
}
|
||||
|
||||
const id = ++this.requestId;
|
||||
const request: JsonRpcRequest = {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
method,
|
||||
params,
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pendingRequests.set(id, { resolve, reject });
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingRequests.delete(id);
|
||||
reject(new Error("Request timeout"));
|
||||
}, 30000);
|
||||
|
||||
this.process!.stdin!.write(JSON.stringify(request) + "\n", (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
this.pendingRequests.delete(id);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the MCP connection
|
||||
*/
|
||||
private async initialize(): Promise<void> {
|
||||
await this.sendRequest("initialize", {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
clientInfo: {
|
||||
name: "codetyper",
|
||||
version: "0.1.0",
|
||||
},
|
||||
});
|
||||
|
||||
// Send initialized notification
|
||||
if (this.process?.stdin) {
|
||||
this.process.stdin.write(
|
||||
JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
method: "notifications/initialized",
|
||||
}) + "\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover available tools and resources
|
||||
*/
|
||||
private async discoverCapabilities(): Promise<void> {
|
||||
// Get tools
|
||||
try {
|
||||
const toolsResult = (await this.sendRequest("tools/list")) as {
|
||||
tools?: MCPToolDefinition[];
|
||||
};
|
||||
this.tools = toolsResult?.tools || [];
|
||||
} catch {
|
||||
this.tools = [];
|
||||
}
|
||||
|
||||
// Get resources
|
||||
try {
|
||||
const resourcesResult = (await this.sendRequest("resources/list")) as {
|
||||
resources?: MCPResourceDefinition[];
|
||||
};
|
||||
this.resources = resourcesResult?.resources || [];
|
||||
} catch {
|
||||
this.resources = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a tool on the server
|
||||
*/
|
||||
async callTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<MCPToolCallResult> {
|
||||
if (this.state !== "connected") {
|
||||
return {
|
||||
success: false,
|
||||
error: "Not connected to server",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.sendRequest("tools/call", {
|
||||
name: toolName,
|
||||
arguments: args,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: result,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a resource from the server
|
||||
*/
|
||||
async readResource(
|
||||
uri: string,
|
||||
): Promise<{ content: string; mimeType?: string } | null> {
|
||||
if (this.state !== "connected") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = (await this.sendRequest("resources/read", { uri })) as {
|
||||
contents?: Array<{ text?: string; mimeType?: string }>;
|
||||
};
|
||||
|
||||
if (result?.contents?.[0]) {
|
||||
return {
|
||||
content: result.contents[0].text || "",
|
||||
mimeType: result.contents[0].mimeType,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the server
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.process) {
|
||||
this.process.kill();
|
||||
this.process = null;
|
||||
}
|
||||
this.state = "disconnected";
|
||||
this.tools = [];
|
||||
this.resources = [];
|
||||
this.pendingRequests.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection state
|
||||
*/
|
||||
getState(): MCPConnectionState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available tools
|
||||
*/
|
||||
getTools(): MCPToolDefinition[] {
|
||||
return this.tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available resources
|
||||
*/
|
||||
getResources(): MCPResourceDefinition[] {
|
||||
return this.resources;
|
||||
}
|
||||
}
|
||||
37
src/services/mcp/index.ts
Normal file
37
src/services/mcp/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* MCP Service - Model Context Protocol integration
|
||||
*
|
||||
* Provides connectivity to MCP servers for extensible tool integration.
|
||||
*/
|
||||
|
||||
export { MCPClient } from "@services/mcp/client";
|
||||
|
||||
export {
|
||||
initializeMCP,
|
||||
loadMCPConfig,
|
||||
saveMCPConfig,
|
||||
connectServer,
|
||||
disconnectServer,
|
||||
connectAllServers,
|
||||
disconnectAllServers,
|
||||
getServerInstances,
|
||||
getAllTools,
|
||||
callTool,
|
||||
addServer,
|
||||
removeServer,
|
||||
getMCPConfig,
|
||||
isMCPAvailable,
|
||||
} from "@services/mcp/manager";
|
||||
|
||||
export type {
|
||||
MCPConfig,
|
||||
MCPServerConfig,
|
||||
MCPServerInstance,
|
||||
MCPToolDefinition,
|
||||
MCPResourceDefinition,
|
||||
MCPToolCallRequest,
|
||||
MCPToolCallResult,
|
||||
MCPConnectionState,
|
||||
MCPTransportType,
|
||||
MCPManagerState,
|
||||
} from "@/types/mcp";
|
||||
297
src/services/mcp/manager.ts
Normal file
297
src/services/mcp/manager.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* MCP Manager - Manages multiple MCP server connections
|
||||
*/
|
||||
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { MCPClient } from "@services/mcp/client";
|
||||
import type {
|
||||
MCPConfig,
|
||||
MCPServerConfig,
|
||||
MCPServerInstance,
|
||||
MCPToolDefinition,
|
||||
MCPToolCallResult,
|
||||
} from "@/types/mcp";
|
||||
|
||||
/**
|
||||
* MCP Configuration file locations
|
||||
*/
|
||||
const CONFIG_LOCATIONS = {
|
||||
global: path.join(os.homedir(), ".codetyper", "mcp.json"),
|
||||
local: path.join(process.cwd(), ".codetyper", "mcp.json"),
|
||||
};
|
||||
|
||||
/**
|
||||
* MCP Manager State
|
||||
*/
|
||||
interface MCPManagerState {
|
||||
clients: Map<string, MCPClient>;
|
||||
config: MCPConfig;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP Manager singleton state
|
||||
*/
|
||||
const state: MCPManagerState = {
|
||||
clients: new Map(),
|
||||
config: { servers: {} },
|
||||
initialized: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Load MCP configuration from file
|
||||
*/
|
||||
const loadConfigFile = async (filePath: string): Promise<MCPConfig | null> => {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
return JSON.parse(content) as MCPConfig;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load MCP configuration (merges global + local)
|
||||
*/
|
||||
export const loadMCPConfig = async (): Promise<MCPConfig> => {
|
||||
const globalConfig = await loadConfigFile(CONFIG_LOCATIONS.global);
|
||||
const localConfig = await loadConfigFile(CONFIG_LOCATIONS.local);
|
||||
|
||||
const merged: MCPConfig = {
|
||||
servers: {
|
||||
...(globalConfig?.servers || {}),
|
||||
...(localConfig?.servers || {}),
|
||||
},
|
||||
};
|
||||
|
||||
return merged;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save MCP configuration
|
||||
*/
|
||||
export const saveMCPConfig = async (
|
||||
config: MCPConfig,
|
||||
global = false,
|
||||
): Promise<void> => {
|
||||
const filePath = global ? CONFIG_LOCATIONS.global : CONFIG_LOCATIONS.local;
|
||||
const dir = path.dirname(filePath);
|
||||
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await fs.writeFile(filePath, JSON.stringify(config, null, 2), "utf-8");
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize MCP Manager
|
||||
*/
|
||||
export const initializeMCP = async (): Promise<void> => {
|
||||
if (state.initialized) return;
|
||||
|
||||
state.config = await loadMCPConfig();
|
||||
state.initialized = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect to a specific MCP server
|
||||
*/
|
||||
export const connectServer = async (
|
||||
serverName: string,
|
||||
): Promise<MCPServerInstance> => {
|
||||
await initializeMCP();
|
||||
|
||||
const serverConfig = state.config.servers[serverName];
|
||||
if (!serverConfig) {
|
||||
throw new Error(`Server '${serverName}' not found in configuration`);
|
||||
}
|
||||
|
||||
// Check if already connected
|
||||
let client = state.clients.get(serverName);
|
||||
if (client && client.getState() === "connected") {
|
||||
return client.getInstance();
|
||||
}
|
||||
|
||||
// Create new client
|
||||
client = new MCPClient({
|
||||
...serverConfig,
|
||||
name: serverName,
|
||||
});
|
||||
|
||||
state.clients.set(serverName, client);
|
||||
|
||||
await client.connect();
|
||||
return client.getInstance();
|
||||
};
|
||||
|
||||
/**
|
||||
* Disconnect from a specific MCP server
|
||||
*/
|
||||
export const disconnectServer = async (serverName: string): Promise<void> => {
|
||||
const client = state.clients.get(serverName);
|
||||
if (client) {
|
||||
await client.disconnect();
|
||||
state.clients.delete(serverName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect to all enabled servers
|
||||
*/
|
||||
export const connectAllServers = async (): Promise<
|
||||
Map<string, MCPServerInstance>
|
||||
> => {
|
||||
await initializeMCP();
|
||||
|
||||
const results = new Map<string, MCPServerInstance>();
|
||||
|
||||
for (const [name, config] of Object.entries(state.config.servers)) {
|
||||
if (config.enabled === false) continue;
|
||||
|
||||
try {
|
||||
const instance = await connectServer(name);
|
||||
results.set(name, instance);
|
||||
} catch (err) {
|
||||
results.set(name, {
|
||||
config: { ...config, name },
|
||||
state: "error",
|
||||
tools: [],
|
||||
resources: [],
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Disconnect from all servers
|
||||
*/
|
||||
export const disconnectAllServers = async (): Promise<void> => {
|
||||
for (const [name] of state.clients) {
|
||||
await disconnectServer(name);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all server instances
|
||||
*/
|
||||
export const getServerInstances = (): Map<string, MCPServerInstance> => {
|
||||
const instances = new Map<string, MCPServerInstance>();
|
||||
|
||||
for (const [name, client] of state.clients) {
|
||||
instances.set(name, client.getInstance());
|
||||
}
|
||||
|
||||
// Include configured but not connected servers
|
||||
for (const [name, config] of Object.entries(state.config.servers)) {
|
||||
if (!instances.has(name)) {
|
||||
instances.set(name, {
|
||||
config: { ...config, name },
|
||||
state: "disconnected",
|
||||
tools: [],
|
||||
resources: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all available tools from all connected servers
|
||||
*/
|
||||
export const getAllTools = (): Array<{
|
||||
server: string;
|
||||
tool: MCPToolDefinition;
|
||||
}> => {
|
||||
const tools: Array<{ server: string; tool: MCPToolDefinition }> = [];
|
||||
|
||||
for (const [name, client] of state.clients) {
|
||||
if (client.getState() === "connected") {
|
||||
for (const tool of client.getTools()) {
|
||||
tools.push({ server: name, tool });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
};
|
||||
|
||||
/**
|
||||
* Call a tool on a specific server
|
||||
*/
|
||||
export const callTool = async (
|
||||
serverName: string,
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<MCPToolCallResult> => {
|
||||
const client = state.clients.get(serverName);
|
||||
if (!client) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Server '${serverName}' not connected`,
|
||||
};
|
||||
}
|
||||
|
||||
return client.callTool(toolName, args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a server to configuration
|
||||
*/
|
||||
export const addServer = async (
|
||||
name: string,
|
||||
config: Omit<MCPServerConfig, "name">,
|
||||
global = false,
|
||||
): Promise<void> => {
|
||||
await initializeMCP();
|
||||
|
||||
const targetConfig = global
|
||||
? (await loadConfigFile(CONFIG_LOCATIONS.global)) || { servers: {} }
|
||||
: (await loadConfigFile(CONFIG_LOCATIONS.local)) || { servers: {} };
|
||||
|
||||
targetConfig.servers[name] = { ...config, name };
|
||||
|
||||
await saveMCPConfig(targetConfig, global);
|
||||
|
||||
// Update in-memory config
|
||||
state.config.servers[name] = { ...config, name };
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a server from configuration
|
||||
*/
|
||||
export const removeServer = async (
|
||||
name: string,
|
||||
global = false,
|
||||
): Promise<void> => {
|
||||
await disconnectServer(name);
|
||||
|
||||
const filePath = global ? CONFIG_LOCATIONS.global : CONFIG_LOCATIONS.local;
|
||||
const config = await loadConfigFile(filePath);
|
||||
|
||||
if (config?.servers[name]) {
|
||||
delete config.servers[name];
|
||||
await saveMCPConfig(config, global);
|
||||
}
|
||||
|
||||
delete state.config.servers[name];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get MCP configuration
|
||||
*/
|
||||
export const getMCPConfig = async (): Promise<MCPConfig> => {
|
||||
await initializeMCP();
|
||||
return state.config;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if MCP is available (has any configured servers)
|
||||
*/
|
||||
export const isMCPAvailable = async (): Promise<boolean> => {
|
||||
await initializeMCP();
|
||||
return Object.keys(state.config.servers).length > 0;
|
||||
};
|
||||
171
src/services/mcp/tools.ts
Normal file
171
src/services/mcp/tools.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* MCP Tools Integration
|
||||
*
|
||||
* Wraps MCP tools to work with the codetyper tool system.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
getAllTools,
|
||||
callTool,
|
||||
connectAllServers,
|
||||
} from "@services/mcp/manager";
|
||||
import type { MCPToolDefinition } from "@/types/mcp";
|
||||
|
||||
/**
|
||||
* Tool definition compatible with codetyper's tool system
|
||||
*/
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: z.ZodSchema;
|
||||
execute: (
|
||||
args: Record<string, unknown>,
|
||||
ctx: unknown,
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
output: string;
|
||||
error?: string;
|
||||
title: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON Schema to Zod schema (simplified)
|
||||
*/
|
||||
const jsonSchemaToZod = (
|
||||
_schema: MCPToolDefinition["inputSchema"],
|
||||
): z.ZodSchema => {
|
||||
// Create a passthrough object schema that accepts any properties
|
||||
// In a full implementation, we'd parse the JSON Schema properly
|
||||
return z.object({}).passthrough();
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a codetyper tool from an MCP tool definition
|
||||
*/
|
||||
const createToolFromMCP = (
|
||||
serverName: string,
|
||||
mcpTool: MCPToolDefinition,
|
||||
): ToolDefinition => {
|
||||
const fullName = `mcp_${serverName}_${mcpTool.name}`;
|
||||
|
||||
return {
|
||||
name: fullName,
|
||||
description:
|
||||
mcpTool.description || `MCP tool: ${mcpTool.name} from ${serverName}`,
|
||||
parameters: jsonSchemaToZod(mcpTool.inputSchema),
|
||||
execute: async (args) => {
|
||||
const result = await callTool(serverName, mcpTool.name, args);
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
output:
|
||||
typeof result.content === "string"
|
||||
? result.content
|
||||
: JSON.stringify(result.content, null, 2),
|
||||
title: `${serverName}/${mcpTool.name}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: "",
|
||||
error: result.error || "Unknown error",
|
||||
title: `${serverName}/${mcpTool.name}`,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all MCP tools as codetyper tool definitions
|
||||
*/
|
||||
export const getMCPTools = async (): Promise<ToolDefinition[]> => {
|
||||
// Ensure servers are connected
|
||||
await connectAllServers();
|
||||
|
||||
const mcpTools = getAllTools();
|
||||
|
||||
return mcpTools.map(({ server, tool }) => createToolFromMCP(server, tool));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get MCP tools for API (OpenAI function format)
|
||||
*/
|
||||
export const getMCPToolsForApi = async (): Promise<
|
||||
Array<{
|
||||
type: "function";
|
||||
function: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: {
|
||||
type: "object";
|
||||
properties: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
};
|
||||
}>
|
||||
> => {
|
||||
await connectAllServers();
|
||||
|
||||
const mcpTools = getAllTools();
|
||||
|
||||
return mcpTools.map(({ server, tool }) => ({
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: `mcp_${server}_${tool.name}`,
|
||||
description: tool.description || `MCP tool: ${tool.name} from ${server}`,
|
||||
parameters: {
|
||||
type: "object" as const,
|
||||
properties: tool.inputSchema.properties || {},
|
||||
required: tool.inputSchema.required,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute an MCP tool by full name (mcp_server_toolname)
|
||||
*/
|
||||
export const executeMCPTool = async (
|
||||
fullName: string,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<{ success: boolean; output: string; error?: string }> => {
|
||||
// Parse the full name: mcp_servername_toolname
|
||||
const match = fullName.match(/^mcp_([^_]+)_(.+)$/);
|
||||
if (!match) {
|
||||
return {
|
||||
success: false,
|
||||
output: "",
|
||||
error: `Invalid MCP tool name: ${fullName}`,
|
||||
};
|
||||
}
|
||||
|
||||
const [, serverName, toolName] = match;
|
||||
const result = await callTool(serverName, toolName, args);
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
output:
|
||||
typeof result.content === "string"
|
||||
? result.content
|
||||
: JSON.stringify(result.content, null, 2),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: "",
|
||||
error: result.error,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a tool name is an MCP tool
|
||||
*/
|
||||
export const isMCPTool = (toolName: string): boolean => {
|
||||
return toolName.startsWith("mcp_");
|
||||
};
|
||||
Reference in New Issue
Block a user