Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.37",
"@modelcontextprotocol/sdk": "^1.7.0",
"@mozilla/readability": "^0.5.0",
"@playwright/test": "^1.50.1",
"@vitest/browser": "^3.0.5",
Expand Down
13 changes: 8 additions & 5 deletions packages/agent/src/core/executeToolCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

const toolContext: ToolContext = { ...context, logger };

let parsedJson: any;

Check warning on line 28 in packages/agent/src/core/executeToolCall.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
try {
parsedJson = JSON.parse(toolCall.content);
} catch (err) {
Expand All @@ -46,7 +46,7 @@
}

// validate JSON schema for input
let validatedJson: any;

Check warning on line 49 in packages/agent/src/core/executeToolCall.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
try {
validatedJson = tool.parameters.parse(parsedJson);
} catch (err) {
Expand Down Expand Up @@ -76,7 +76,7 @@
});
}

let output: any;

Check warning on line 79 in packages/agent/src/core/executeToolCall.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
try {
output = await tool.execute(validatedJson, toolContext);
} catch (err) {
Expand Down Expand Up @@ -110,9 +110,12 @@
}
}

const toolOutput =
typeof output === 'string' ? output : JSON.stringify(output, null, 2);
return toolOutput.length > OUTPUT_LIMIT
? `${toolOutput.slice(0, OUTPUT_LIMIT)}...(truncated)`
: toolOutput;
const outputIsString = typeof output === 'string';
if (outputIsString) {
return output.length > OUTPUT_LIMIT
? `${output.slice(0, OUTPUT_LIMIT)}...(truncated)`
: output;
} else {
return JSON.stringify(output, null, 2);
}
};
35 changes: 35 additions & 0 deletions packages/agent/src/core/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Model Context Protocol (MCP) Integration
*
* This module provides integration with the Model Context Protocol (MCP),
* allowing MyCoder to use context from MCP-compatible servers.
*
* Uses the official MCP SDK: https://www.npmjs.com/package/@modelcontextprotocol/sdk
*/

/**
* Configuration for MCP in mycoder.config.js
*/
export interface McpConfig {
/** Array of MCP server configurations */
servers?: McpServerConfig[];
/** Default resources to load automatically */
defaultResources?: string[];
}

/**
* Configuration for an MCP server
*/
export interface McpServerConfig {
/** Unique name for this MCP server */
name: string;
/** URL of the MCP server */
url: string;
/** Optional authentication configuration */
auth?: {
/** Authentication type (currently only 'bearer' is supported) */
type: 'bearer';
/** Authentication token */
token: string;
};
}
13 changes: 10 additions & 3 deletions packages/agent/src/core/toolAgent/toolExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ import { Tool, ToolCall, ToolContext } from '../types.js';
import { addToolResultToMessages } from './messageUtils.js';
import { ToolCallResult } from './types.js';

const safeParse = (value: string) => {
const safeParse = (value: string, context: Record<string, string>) => {
try {
return JSON.parse(value);
} catch (error) {
console.error('Error parsing JSON:', error, 'original value:', value);
console.error(
'Error parsing JSON:',
error,
'original value:',
value,
'context',
JSON.stringify(context),
);
return { error: value };
}
};
Expand Down Expand Up @@ -77,7 +84,7 @@ export async function executeTools(
}
}

const parsedResult = safeParse(toolResult);
const parsedResult = safeParse(toolResult, { tool: call.name });

// Add the tool result to messages
addToolResultToMessages(messages, call.id, parsedResult, isError);
Expand Down
2 changes: 2 additions & 0 deletions packages/agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export * from './core/toolAgent/toolExecutor.js';
export * from './core/toolAgent/tokenTracking.js';
export * from './core/toolAgent/types.js';
export * from './core/llm/provider.js';
// MCP
export * from './core/mcp/index.js';

// Utils
export * from './tools/getTools.js';
Expand Down
10 changes: 10 additions & 0 deletions packages/agent/src/tools/getTools.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { McpConfig } from '../core/mcp/index.js';
import { Tool } from '../core/types.js';

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

interface GetToolsOptions {
userPrompt?: boolean;
mcpConfig?: McpConfig;
}

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

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

// Add MCP tool if we have any servers configured
if (mcpConfig.servers && mcpConfig.servers.length > 0) {
const mcpTool = createMcpTool(mcpConfig);
tools.push(mcpTool);
}

return tools;
}
208 changes: 208 additions & 0 deletions packages/agent/src/tools/mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/**
* MCP Tool for MyCoder Agent
*
* This tool allows the agent to interact with Model Context Protocol (MCP) servers
* to retrieve resources and use tools provided by those servers.
*
* Uses the official MCP SDK: https://www.npmjs.com/package/@modelcontextprotocol/sdk
*/

import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

import { McpConfig } from '../core/mcp/index.js';
import { Tool } from '../core/types.js';

// Parameters for listResources method
const listResourcesSchema = z.object({
server: z
.string()
.optional()
.describe('Optional server name to filter resources by'),
});

// Parameters for getResource method
const getResourceSchema = z.object({
uri: z
.string()
.describe('URI of the resource to fetch in the format "scheme://path"'),
});

// Return type for listResources
const listResourcesReturnSchema = z.array(
z.object({
uri: z.string(),
metadata: z.record(z.unknown()).optional(),
}),
);

// Return type for getResource
const getResourceReturnSchema = z.string();

// Map to store MCP clients
const mcpClients = new Map<string, any>();

/**
* Create a new MCP tool with the specified configuration
* @param config MCP configuration
* @returns The MCP tool
*/
export function createMcpTool(config: McpConfig): Tool {
// We'll import the MCP SDK dynamically to avoid TypeScript errors
// This is a temporary solution until we can properly add type declarations
const mcpSdk = require('@modelcontextprotocol/sdk');

// Initialize MCP clients for each configured server
mcpClients.clear();

if (config.servers && config.servers.length > 0) {
for (const server of config.servers) {
try {
let clientOptions: any = {
baseURL: server.url,
};

// Add authentication if configured
if (server.auth && server.auth.type === 'bearer') {
clientOptions = {
...clientOptions,
headers: {
Authorization: `Bearer ${server.auth.token}`,
},
};
}

const client = new mcpSdk.Client(clientOptions);
mcpClients.set(server.name, client);
} catch (error) {
console.error(
`Failed to initialize MCP client for server ${server.name}:`,
error,
);
}
}
}

// Define the MCP tool
return {
name: 'mcp',
description:
'Interact with Model Context Protocol (MCP) servers to retrieve resources',
parameters: z.discriminatedUnion('method', [
z.object({
method: z.literal('listResources'),
params: listResourcesSchema.optional(),
}),
z.object({
method: z.literal('getResource'),
params: getResourceSchema,
}),
]),
parametersJsonSchema: zodToJsonSchema(
z.discriminatedUnion('method', [
z.object({
method: z.literal('listResources'),
params: listResourcesSchema.optional(),
}),
z.object({
method: z.literal('getResource'),
params: getResourceSchema,
}),
]),
),
returns: z.union([listResourcesReturnSchema, getResourceReturnSchema]),
returnsJsonSchema: zodToJsonSchema(
z.union([listResourcesReturnSchema, getResourceReturnSchema]),
),

execute: async ({ method, params }, { logger }) => {
// Extract the server name from a resource URI
function getServerNameFromUri(uri: string): string | undefined {
const match = uri.match(/^([^:]+):\/\//);
return match ? match[1] : undefined;
}

if (method === 'listResources') {
// List available resources from MCP servers
const resources: any[] = [];
const serverFilter = params?.server;

// If a specific server is requested, only check that server
if (serverFilter) {
const client = mcpClients.get(serverFilter);
if (client) {
try {
logger.verbose(`Fetching resources from server: ${serverFilter}`);
const serverResources = await client.resources();
resources.push(...(serverResources as any[]));
} catch (error) {
logger.error(
`Failed to fetch resources from server ${serverFilter}:`,
error,
);
}
} else {
logger.warn(`Server not found: ${serverFilter}`);
}
} else {
// Otherwise, check all servers
for (const [serverName, client] of mcpClients.entries()) {
try {
logger.verbose(`Fetching resources from server: ${serverName}`);
const serverResources = await client.resources();
resources.push(...(serverResources as any[]));
} catch (error) {
logger.error(
`Failed to fetch resources from server ${serverName}:`,
error,
);
}
}
}

return resources;
} else if (method === 'getResource') {
// Fetch a resource from an MCP server
const uri = params.uri;

// Parse the URI to determine which server to use
const serverName = getServerNameFromUri(uri);
if (!serverName) {
throw new Error(`Could not determine server from URI: ${uri}`);
}

const client = mcpClients.get(serverName);
if (!client) {
throw new Error(`Server not found: ${serverName}`);
}

// Use the MCP SDK to fetch the resource
logger.verbose(`Fetching resource: ${uri}`);
const resource = await client.resource(uri);
return resource.content;
}

throw new Error(`Unknown method: ${method}`);
},

logParameters: (params, { logger }) => {
if (params.method === 'listResources') {
logger.verbose(
`Listing MCP resources${params.params?.server ? ` from server: ${params.params.server}` : ''}`,
);
} else if (params.method === 'getResource') {
logger.verbose(`Fetching MCP resource: ${params.params.uri}`);
}
},

logReturns: (result, { logger }) => {
if (Array.isArray(result)) {
logger.verbose(`Found ${result.length} MCP resources`);
} else {
logger.verbose(
`Retrieved MCP resource content (${result.length} characters)`,
);
}
},
};
}
Loading
Loading