8000 feat: implement MCP tools support · drivecore/mycoder@2d99ac8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2d99ac8

Browse files
committed
feat: implement MCP tools support
1 parent 12e9d42 commit 2d99ac8

File tree

5 files changed

+474
-10
lines changed

5 files changed

+474
-10
lines changed

packages/agent/src/core/mcp/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export interface McpConfig {
1515
servers?: McpServerConfig[];
1616
/** Default resources to load automatically */
1717
defaultResources?: string[];
18+
/** Default tools to make available */
19+
defaultTools?: string[];
1820
}
1921

2022
/**

packages/agent/src/tools/browser/filterPageContent.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Readability } from '@mozilla/readability';
22
import { JSDOM } from 'jsdom';
33
import { Page } from 'playwright';
44

5+
const OUTPUT_LIMIT = 11 * 1024; // 10KB limit
6+
57
/**
68
* Returns the raw HTML content of the page without any processing
79
*/
@@ -93,13 +95,22 @@ export async function filterPageContent(
9395
page: Page,
9496
pageFilter: 'simple' | 'none' | 'readability',
9597
): Promise<string> {
98+
let result: string = '';
9699
switch (pageFilter) {
97100
case 'none':
98-
return getNoneProcessedDOM(page);
101+
result = await getNoneProcessedDOM(page);
102+
break;
99103
case 'readability':
100-
return getReadabilityProcessedDOM(page);
104+
result = await getReadabilityProcessedDOM(page);
105+
break;
101106
case 'simple':
102107
default:
103-
return getSimpleProcessedDOM(page);
108+
result = await getSimpleProcessedDOM(page);
109+
break;
110+
}
111+
112+
if (result.length > OUTPUT_LIMIT) {
113+
return result.slice(0, OUTPUT_LIMIT) + '...(truncated)';
104114
}
115+
return result;
105116
}

packages/agent/src/tools/mcp.test.ts

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
3+
import { McpConfig } from '../core/mcp/index.js';
4+
import { ToolContext } from '../core/types.js';
5+
6+
import { createMcpTool } from './mcp.js';
7+
8+
// Mock the require function to mock the MCP SDK
9+
vi.mock('@modelcontextprotocol/sdk', () => {
10+
return {
11+
default: {
12+
Client: vi.fn().mockImplementation(() => ({
13+
resources: vi.fn().mockResolvedValue([
14+
{ uri: 'test://resource1', metadata: { title: 'Resource 1' } },
15+
{ uri: 'test://resource2', metadata: { title: 'Resource 2' } },
16+
]),
17+
resource: vi.fn().mockImplementation((uri) => {
18+
if (uri === 'test://resource1') {
19+
return Promise.resolve({ content: 'Resource 1 content' });
20+
} else if (uri === 'test://resource2') {
21+
return Promise.resolve({ content: 'Resource 2 content' });
22+
}
23+
return Promise.reject(new Error(`Resource not found: ${uri}`));
24+
}),
25+
tools: vi.fn().mockResolvedValue([
26+
{
27+
uri: 'test://tool1',
28+
name: 'Tool 1',
29+
description: 'Test tool 1',
30+
parameters: { param1: { type: 'string' } },
31+
returns: { type: 'string' },
32+
},
33+
{
34+
uri: 'test://tool2',
35+
name: 'Tool 2',
36+
description: 'Test tool 2',
37+
parameters: { param1: { type: 'number' } },
38+
returns: { type: 'object' },
39+
},
40+
]),
41+
tool: vi.fn().mockImplementation((uri, params) => {
42+
if (uri === 'test://tool1') {
43+
return Promise.resolve(
44+
`Tool 1 executed with params: ${JSON.stringify(params)}`,
45+
);
46+
} else if (uri === 'test://tool2') {
47+
return Promise.resolve({
48+
result: `Tool 2 executed with params: ${JSON.stringify(params)}`,
49+
});
50+
}
51+
return Promise.reject(new Error(`Tool not found: ${uri}`));
52+
}),
53+
})),
54+
},
55+
Client: vi.fn().mockImplementation(() => ({
56+
resources: vi.fn().mockResolvedValue([
57+
{ uri: 'test://resource1', metadata: { title: 'Resource 1' } },
58+
{ uri: 'test://resource2', metadata: { title: 'Resource 2' } },
59+
]),
60+
resource: vi.fn().mockImplementation((uri) => {
61+
if (uri === 'test://resource1') {
62+
return Promise.resolve({ content: 'Resource 1 content' });
63+
} else if (uri === 'test://resource2') {
64+
return Promise.resolve({ content: 'Resource 2 content' });
65+
}
66+
return Promise.reject(new Error(`Resource not found: ${uri}`));
67+
}),
68+
tools: vi.fn().mockResolvedValue([
69+
{
70+
uri: 'test://tool1',
71+
name: 'Tool 1',
72+
description: 'Test tool 1',
73+
parameters: { param1: { type: 'string' } },
74+
returns: { type: 'string' },
75+
},
76+
{
77+
uri: 'test://tool2',
78+
name: 'Tool 2',
79+
description: 'Test tool 2',
80+
parameters: { param1: { type: 'number' } },
81+
returns: { type: 'object' },
82+
},
83+
]),
84+
tool: vi.fn().mockImplementation((uri, params) => {
85+
if (uri === 'test://tool1') {
86+
return Promise.resolve(
87+
`Tool 1 executed with params: ${JSON.stringify(params)}`,
88+
);
89+
} else if (uri === 'test://tool2') {
90+
return Promise.resolve({
91+
result: `Tool 2 executed with params: ${JSON.stringify(params)}`,
92+
});
93+
}
94+
return Promise.reject(new Error(`Tool not found: ${uri}`));
95+
}),
96+
})),
97+
};
98+
});
99+
100+
// Create a mock logger
101+
const mockLogger = {
102+
verbose: vi.fn(),
103+
info: vi.fn(),
104+
warn: vi.fn(),
105+
error: vi.fn(),
106+
debug: vi.fn(),
107+
log: vi.fn(),
108+
};
109+
110+
// Create a mock TokenTracker
111+
const mockTokenTracker = {
112+
tokenUsage: {
113+
input: 0,
114+
output: 0,
115+
cacheReads: 0,
116+
cacheWrites: 0,
117+
clone: vi.fn().mockReturnThis(),
118+
add: vi.fn(),
119+
getCost: vi.fn().mockReturnValue('$0.00'),
120+
toString: vi
121+
.fn()
122+
.mockReturnValue(
123+
'input: 0 cache-writes: 0 cache-reads: 0 output: 0 COST: $0.00',
124+
),
125+
},
126+
children: [],
127+
name: 'test',
128+
getTotalUsage: vi.fn().mockReturnValue({
129+
input: 0,
130+
output: 0,
131+
cacheReads: 0,
132+
cacheWrites: 0,
133+
clone: vi.fn().mockReturnThis(),
134+
add: vi.fn(),
135+
getCost: vi.fn().mockReturnValue('$0.00'),
136+
toString: vi.fn(),
137+
}),
138+
getTotalCost: vi.fn().mockReturnValue('$0.00'),
139+
toString: vi
140+
.fn()
141+
.mockReturnValue(
142+
'test: input: 0 cache-writes: 0 cache-reads: 0 output: 0 COST: $0.00',
143+
),
144+
};
145+
146+
// Create a mock BackgroundTools
147+
const mockBackgroundTools = {
148+
tools: new Map(),
149+
ownerName: 'test',
150+
registerShell: vi.fn().mockReturnValue('shell-id'),
151+
registerBrowser: vi.fn().mockReturnValue('browser-id'),
152+
registerAgent: vi.fn().mockReturnValue('agent-id'),
153+
updateToolStatus: vi.fn().mockReturnValue(true),
154+
getTools: vi.fn().mockReturnValue([]),
155+
getToolById: vi.fn().mockReturnValue(undefined),
156+
cleanup: vi.fn().mockResolvedValue(undefined),
157+
};
158+
159+
// Create a mock ToolContext
160+
const mockToolContext: ToolContext = {
161+
logger: mockLogger as any,
162+
workingDirectory: '/test',
163+
headless: true,
164+
userSession: false,
165+
pageFilter: 'none',
166+
tokenTracker: mockTokenTracker as any,
167+
githubMode: false,
168+
provider: 'anthropic',
169+
model: 'claude-3',
170+
maxTokens: 4096,
171+
temperature: 0.7,
172+
backgroundTools: mockBackgroundTools as any,
173+
};
174+
175+
describe('MCP Tool', () => {
176+
let mcpTool: ReturnType<typeof createMcpTool>;
177+
const testConfig: McpConfig = {
178+
servers: [
179+
{
180+
name: 'test',
181+
url: 'https://test.example.com',
182+
auth: {
183+
type: 'bearer',
184+
token: 'test-token',
185+
},
186+
},
187+
],
188+
};
189+
190+
beforeEach(() => {
191+
mcpTool = createMcpTool(testConfig);
192+
});
193+
194+
afterEach(() => {
195+
vi.clearAllMocks();
196+
});
197+
198+
it('should list resources from MCP servers', async () => {
199+
const result = await mcpTool.execute(
200+
{ method: 'listResources', params: { server: 'test' } },
201+
mockToolContext,
202+
);
203+
204+
expect(result).toHaveLength(2);
205+
expect(result[0].uri).toBe('test://resource1');
206+
expect(result[1].uri).toBe('test://resource2');
207+
expect(mockLogger.verbose).toHaveBeenCalledWith(
208+
'Fetching resources from server: test',
209+
);
210+
});
211+
212+
it('should get a resource from an MCP server', async () => {
213+
const result = await mcpTool.execute(
214+
{ method: 'getResource', params: { uri: 'test://resource1' } },
215+
mockToolContext,
216+
);
217+
218+
expect(result).toBe('Resource 1 content');
219+
expect(mockLogger.verbose).toHaveBeenCalledWith(
220+
'Fetching resource: test://resource1',
221+
);
222+
});
223+
224+
it('should list tools from MCP servers', async () => {
225+
const result = await mcpTool.execute(
226+
{ method: 'listTools', params: { server: 'test' } },
227+
mockToolContext,
228+
);
229+
230+
expect(result).toHaveLength(2);
231+
expect(result[0].uri).toBe('test://tool1');
232+
expect(result[0].name).toBe('Tool 1');
233+
expect(result[1].uri).toBe('test://tool2');
234+
expect(result[1].name).toBe('Tool 2');
235+
expect(mockLogger.verbose).toHaveBeenCalledWith(
236+
'Fetching tools from server: test',
237+
);
238+
});
239+
240+
it('should execute a tool from an MCP server', async () => {
241+
const result = await mcpTool.execute(
242+
{
243+
method: 'executeTool',
244+
params: { uri: 'test://tool1', params: { param1: 'test' } },
245+
},
246+
mockToolContext,
247+
);
248+
249+
expect(result).toBe('Tool 1 executed with params: {"param1":"test"}');
250+
expect(mockLogger.verbose).toHaveBeenCalledWith(
251+
'Executing tool: test://tool1 with params:',
252+
{ param1: 'test' },
253+
);
254+
});
255+
256+
it('should execute a tool that returns an object', async () => {
257+
const result = await mcpTool.execute(
258+
{
259+
method: 'executeTool',
260+
params: { uri: 'test://tool2', params: { param1: 42 } },
261+
},
262+
mockToolContext,
263+
);
264+
265+
expect(result).toEqual({
266+
result: 'Tool 2 executed with params: {"param1":42}',
267+
});
268+
});
269+
270+
it('should throw an error for unknown methods', async () => {
271+
await expect(
272+
mcpTool.execute(
273+
{ method: 'unknownMethod' as any, params: {} },
274+
mockToolContext,
275+
),
276+
).rejects.toThrow('Unknown method: unknownMethod');
277+
});
278+
279+
it('should throw an error for invalid URIs', async () => {
280+
await expect(
281+
mcpTool.execute(
282+
{ method: 'getResource', params: { uri: 'invalid-uri' } },
283+
mockToolContext,
284+
),
285+
).rejects.toThrow('Could not determine server from URI: invalid-uri');
286+
});
287+
288+
it('should throw an error for unknown servers', async () => {
289+
await expect(
290+
mcpTool.execute(
291+
{ method: 'getResource', params: { uri: 'unknown://resource' } },
292+
mockToolContext,
293+
),
294+
).rejects.toThrow('Server not found: unknown');
295+
});
296+
});

0 commit comments

Comments
 (0)
0