Skip to content

Commit 9398c56

Browse files
authored
Merge pull request #242 from drivecore/feature/mcp-support
Add Basic Model Context Protocol (MCP) Support
2 parents 92a5f88 + a9904b8 commit 9398c56

File tree

11 files changed

+839
-38
lines changed

11 files changed

+839
-38
lines changed

packages/agent/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"license": "MIT",
4646
"dependencies": {
4747
"@anthropic-ai/sdk": "^0.37",
48+
"@modelcontextprotocol/sdk": "^1.7.0",
4849
"@mozilla/readability": "^0.5.0",
4950
"@playwright/test": "^1.50.1",
5051
"@vitest/browser": "^3.0.5",

packages/agent/src/core/executeToolCall.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,12 @@ export const executeToolCall = async (
110110
}
111111
}
112112

113-
const toolOutput =
114-
typeof output === 'string' ? output : JSON.stringify(output, null, 2);
115-
return toolOutput.length > OUTPUT_LIMIT
116-
? `${toolOutput.slice(0, OUTPUT_LIMIT)}...(truncated)`
117-
: toolOutput;
113+
const outputIsString = typeof output === 'string';
114+
if (outputIsString) {
115+
return output.length > OUTPUT_LIMIT
116+
? `${output.slice(0, OUTPUT_LIMIT)}...(truncated)`
117+
: output;
118+
} else {
119+
return JSON.stringify(output, null, 2);
120+
}
118121
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Model Context Protocol (MCP) Integration
3+
*
4+
* This module provides integration with the Model Context Protocol (MCP),
5+
* allowing MyCoder to use context from MCP-compatible servers.
6+
*
7+
* Uses the official MCP SDK: https://www.npmjs.com/package/@modelcontextprotocol/sdk
8+
*/
9+
10+
/**
11+
* Configuration for MCP in mycoder.config.js
12+
*/
13+
export interface McpConfig {
14+
/** Array of MCP server configurations */
15+
servers?: McpServerConfig[];
16+
/** Default resources to load automatically */
17+
defaultResources?: string[];
18+
}
19+
20+
/**
21+
* Configuration for an MCP server
22+
*/
23+
export interface McpServerConfig {
24+
/** Unique name for this MCP server */
25+
name: string;
26+
/** URL of the MCP server */
27+
url: string;
28+
/** Optional authentication configuration */
29+
auth?: {
30+
/** Authentication type (currently only 'bearer' is supported) */
31+
type: 'bearer';
32+
/** Authentication token */
33+
token: string;
34+
};
35+
}

packages/agent/src/core/toolAgent/toolExecutor.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@ import { Tool, ToolCall, ToolContext } from '../types.js';
66
import { addToolResultToMessages } from './messageUtils.js';
77
import { ToolCallResult } from './types.js';
88

9-
const safeParse = (value: string) => {
9+
const safeParse = (value: string, context: Record<string, string>) => {
1010
try {
1111
return JSON.parse(value);
1212
} catch (error) {
13-
console.error('Error parsing JSON:', error, 'original value:', value);
13+
console.error(
14+
'Error parsing JSON:',
15+
error,
16+
'original value:',
17+
value,
18+
'context',
19+
JSON.stringify(context),
20+
);
1421
return { error: value };
1522
}
1623
};
@@ -77,7 +84,7 @@ export async function executeTools(
7784
}
7885
}
7986

80-
const parsedResult = safeParse(toolResult);
87+
const parsedResult = safeParse(toolResult, { tool: call.name });
8188

8289
// Add the tool result to messages
8390
addToolResultToMessages(messages, call.id, parsedResult, isError);

packages/agent/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export * from './core/toolAgent/toolExecutor.js';
3535
export * from './core/toolAgent/tokenTracking.js';
3636
export * from './core/toolAgent/types.js';
3737
export * from './core/llm/provider.js';
38+
// MCP
39+
export * from './core/mcp/index.js';
3840

3941
// Utils
4042
export * from './tools/getTools.js';

packages/agent/src/tools/getTools.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { McpConfig } from '../core/mcp/index.js';
12
import { Tool } from '../core/types.js';
23

34
// Import tools
@@ -8,6 +9,7 @@ import { agentStartTool } from './interaction/agentStart.js';
89
import { userPromptTool } from './interaction/userPrompt.js';
910
import { fetchTool } from './io/fetch.js';
1011
import { textEditorTool } from './io/textEditor.js';
12+
import { createMcpTool } from './mcp.js';
1113
import { listBackgroundToolsTool } from './system/listBackgroundTools.js';
1214
import { respawnTool } from './system/respawn.js';
1315
import { sequenceCompleteTool } from './system/sequenceComplete.js';
@@ -19,10 +21,12 @@ import { sleepTool } from './system/sleep.js';
1921

2022
interface GetToolsOptions {
2123
userPrompt?: boolean;
24+
mcpConfig?: McpConfig;
2225
}
2326

2427
export function getTools(options?: GetToolsOptions): Tool[] {
2528
const userPrompt = options?.userPrompt !== false; // Default to true if not specified
29+
const mcpConfig = options?.mcpConfig || { servers: [], defaultResources: [] };
2630

2731
// Force cast to Tool type to avoid TypeScript issues
2832
const tools: Tool[] = [
@@ -45,5 +49,11 @@ export function getTools(options?: GetToolsOptions): Tool[] {
4549
tools.push(userPromptTool as unknown as Tool);
4650
}
4751

52+
// Add MCP tool if we have any servers configured
53+
if (mcpConfig.servers && mcpConfig.servers.length > 0) {
54+
const mcpTool = createMcpTool(mcpConfig);
55+
tools.push(mcpTool);
56+
}
57+
4858
return tools;
4959
}

packages/agent/src/tools/mcp.ts

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/**
2+
* MCP Tool for MyCoder Agent
3+
*
4+
* This tool allows the agent to interact with Model Context Protocol (MCP) servers
5+
* to retrieve resources and use tools provided by those servers.
6+
*
7+
* Uses the official MCP SDK: https://www.npmjs.com/package/@modelcontextprotocol/sdk
8+
*/
9+
10+
import { z } from 'zod';
11+
import { zodToJsonSchema } from 'zod-to-json-schema';
12+
13+
import { McpConfig } from '../core/mcp/index.js';
14+
import { Tool } from '../core/types.js';
15+
16+
// Parameters for listResources method
17+
const listResourcesSchema = z.object({
18+
server: z
19+
.string()
20+
.optional()
21+
.describe('Optional server name to filter resources by'),
22+
});
23+
24+
// Parameters for getResource method
25+
const getResourceSchema = z.object({
26+
uri: z
27+
.string()
28+
.describe('URI of the resource to fetch in the format "scheme://path"'),
29+
});
30+
31+
// Return type for listResources
32+
const listResourcesReturnSchema = z.array(
33+
z.object({
34+
uri: z.string(),
35+
metadata: z.record(z.unknown()).optional(),
36+
}),
37+
);
38+
39+
// Return type for getResource
40+
const getResourceReturnSchema = z.string();
41+
42+
// Map to store MCP clients
43+
const mcpClients = new Map<string, any>();
44+
45+
/**
46+
* Create a new MCP tool with the specified configuration
47+
* @param config MCP configuration
48+
* @returns The MCP tool
49+
*/
50+
export function createMcpTool(config: McpConfig): Tool {
51+
// We'll import the MCP SDK dynamically to avoid TypeScript errors
52+
// This is a temporary solution until we can properly add type declarations
53+
const mcpSdk = require('@modelcontextprotocol/sdk');
54+
55+
// Initialize MCP clients for each configured server
56+
mcpClients.clear();
57+
58+
if (config.servers && config.servers.length > 0) {
59+
for (const server of config.servers) {
60+
try {
61+
let clientOptions: any = {
62+
baseURL: server.url,
63+
};
64+
65+
// Add authentication if configured
66+
if (server.auth && server.auth.type === 'bearer') {
67+
clientOptions = {
68+
...clientOptions,
69+
headers: {
70+
Authorization: `Bearer ${server.auth.token}`,
71+
},
72+
};
73+
}
74+
75+
const client = new mcpSdk.Client(clientOptions);
76+
mcpClients.set(server.name, client);
77+
} catch (error) {
78+
console.error(
79+
`Failed to initialize MCP client for server ${server.name}:`,
80+
error,
81+
);
82+
}
83+
}
84+
}
85+
86+
// Define the MCP tool
87+
return {
88+
name: 'mcp',
89+
description:
90+
'Interact with Model Context Protocol (MCP) servers to retrieve resources',
91+
parameters: z.discriminatedUnion('method', [
92+
z.object({
93+
method: z.literal('listResources'),
94+
params: listResourcesSchema.optional(),
95+
}),
96+
z.object({
97+
method: z.literal('getResource'),
98+
params: getResourceSchema,
99+
}),
100+
]),
101+
parametersJsonSchema: zodToJsonSchema(
102+
z.discriminatedUnion('method', [
103+
z.object({
104+
method: z.literal('listResources'),
105+
params: listResourcesSchema.optional(),
106+
}),
107+
z.object({
108+
method: z.literal('getResource'),
109+
params: getResourceSchema,
110+
}),
111+
]),
112+
),
113+
returns: z.union([listResourcesReturnSchema, getResourceReturnSchema]),
114+
returnsJsonSchema: zodToJsonSchema(
115+
z.union([listResourcesReturnSchema, getResourceReturnSchema]),
116+
),
117+
118+
execute: async ({ method, params }, { logger }) => {
119+
// Extract the server name from a resource URI
120+
function getServerNameFromUri(uri: string): string | undefined {
121+
const match = uri.match(/^([^:]+):\/\//);
122+
return match ? match[1] : undefined;
123+
}
124+
125+
if (method === 'listResources') {
126+
// List available resources from MCP servers
127+
const resources: any[] = [];
128+
const serverFilter = params?.server;
129+
130+
// If a specific server is requested, only check that server
131+
if (serverFilter) {
132+
const client = mcpClients.get(serverFilter);
133+
if (client) {
134+
try {
135+
logger.verbose(`Fetching resources from server: ${serverFilter}`);
136+
const serverResources = await client.resources();
137+
resources.push(...(serverResources as any[]));
138+
} catch (error) {
139+
logger.error(
140+
`Failed to fetch resources from server ${serverFilter}:`,
141+
error,
142+
);
143+
}
144+
} else {
145+
logger.warn(`Server not found: ${serverFilter}`);
146+
}
147+
} else {
148+
// Otherwise, check all servers
149+
for (const [serverName, client] of mcpClients.entries()) {
150+
try {
151+
logger.verbose(`Fetching resources from server: ${serverName}`);
152+
const serverResources = await client.resources();
153+
resources.push(...(serverResources as any[]));
154+
} catch (error) {
155+
logger.error(
156+
`Failed to fetch resources from server ${serverName}:`,
157+
error,
158+
);
159+
}
160+
}
161+
}
162+
163+
return resources;
164+
} else if (method === 'getResource') {
165+
// Fetch a resource from an MCP server
166+
const uri = params.uri;
167+
168+
// Parse the URI to determine which server to use
169+
const serverName = getServerNameFromUri(uri);
170+
if (!serverName) {
171+
throw new Error(`Could not determine server from URI: ${uri}`);
172+
}
173+
174+
const client = mcpClients.get(serverName);
175+
if (!client) {
176+
throw new Error(`Server not found: ${serverName}`);
177+
}
178+
179+
// Use the MCP SDK to fetch the resource
180+
logger.verbose(`Fetching resource: ${uri}`);
181+
const resource = await client.resource(uri);
182+
return resource.content;
183+
}
184+
185+
throw new Error(`Unknown method: ${method}`);
186+
},
187+
188+
logParameters: (params, { logger }) => {
189+
if (params.method === 'listResources') {
190+
logger.verbose(
191+
`Listing MCP resources${params.params?.server ? ` from server: ${params.params.server}` : ''}`,
192+
);
193+
} else if (params.method === 'getResource') {
194+
logger.verbose(`Fetching MCP resource: ${params.params.uri}`);
195+
}
196+
},
197+
198+
logReturns: (result, { logger }) => {
199+
if (Array.isArray(result)) {
200+
logger.verbose(`Found ${result.length} MCP resources`);
201+
} else {
202+
logger.verbose(
203+
`Retrieved MCP resource content (${result.length} characters)`,
204+
);
205+
}
206+
},
207+
};
208+
}

0 commit comments

Comments
 (0)