diff --git a/packages/agent/src/core/mcp/index.ts b/packages/agent/src/core/mcp/index.ts index 83f785a..daee4fa 100644 --- a/packages/agent/src/core/mcp/index.ts +++ b/packages/agent/src/core/mcp/index.ts @@ -15,6 +15,8 @@ export interface McpConfig { servers?: McpServerConfig[]; /** Default resources to load automatically */ defaultResources?: string[]; + /** Default tools to make available */ + defaultTools?: string[]; } /** diff --git a/packages/agent/src/tools/browser/filterPageContent.ts b/packages/agent/src/tools/browser/filterPageContent.ts index 398cc1e..9ddad7e 100644 --- a/packages/agent/src/tools/browser/filterPageContent.ts +++ b/packages/agent/src/tools/browser/filterPageContent.ts @@ -2,6 +2,8 @@ import { Readability } from '@mozilla/readability'; import { JSDOM } from 'jsdom'; import { Page } from 'playwright'; +const OUTPUT_LIMIT = 11 * 1024; // 10KB limit + /** * Returns the raw HTML content of the page without any processing */ @@ -93,13 +95,22 @@ export async function filterPageContent( page: Page, pageFilter: 'simple' | 'none' | 'readability', ): Promise { + let result: string = ''; switch (pageFilter) { case 'none': - return getNoneProcessedDOM(page); + result = await getNoneProcessedDOM(page); + break; case 'readability': - return getReadabilityProcessedDOM(page); + result = await getReadabilityProcessedDOM(page); + break; case 'simple': default: - return getSimpleProcessedDOM(page); + result = await getSimpleProcessedDOM(page); + break; + } + + if (result.length > OUTPUT_LIMIT) { + return result.slice(0, OUTPUT_LIMIT) + '...(truncated)'; } + return result; } diff --git a/packages/agent/src/tools/mcp.ts b/packages/agent/src/tools/mcp.ts index f853441..6e92917 100644 --- a/packages/agent/src/tools/mcp.ts +++ b/packages/agent/src/tools/mcp.ts @@ -28,6 +28,25 @@ const getResourceSchema = z.object({ .describe('URI of the resource to fetch in the format "scheme://path"'), }); +// Parameters for listTools method +const listToolsSchema = z.object({ + server: z + .string() + .optional() + .describe('Optional server name to filter tools by'), +}); + +// Parameters for executeTool method +const executeToolSchema = z.object({ + uri: z + .string() + .describe('URI of the tool to execute in the format "scheme://path"'), + params: z + .record(z.unknown()) + .optional() + .describe('Parameters to pass to the tool'), +}); + // Return type for listResources const listResourcesReturnSchema = z.array( z.object({ @@ -39,6 +58,20 @@ const listResourcesReturnSchema = z.array( // Return type for getResource const getResourceReturnSchema = z.string(); +// Return type for listTools +const listToolsReturnSchema = z.array( + z.object({ + uri: z.string(), + name: z.string(), + description: z.string().optional(), + parameters: z.record(z.unknown()).optional(), + returns: z.record(z.unknown()).optional(), + }), +); + +// Return type for executeTool - can be any JSON value +const executeToolReturnSchema = z.unknown(); + // Map to store MCP clients const mcpClients = new Map(); @@ -87,7 +120,7 @@ export function createMcpTool(config: McpConfig): Tool { return { name: 'mcp', description: - 'Interact with Model Context Protocol (MCP) servers to retrieve resources', + 'Interact with Model Context Protocol (MCP) servers to retrieve resources and execute tools', parameters: z.discriminatedUnion('method', [ z.object({ method: z.literal('listResources'), @@ -97,6 +130,14 @@ export function createMcpTool(config: McpConfig): Tool { method: z.literal('getResource'), params: getResourceSchema, }), + z.object({ + method: z.literal('listTools'), + params: listToolsSchema.optional(), + }), + z.object({ + method: z.literal('executeTool'), + params: executeToolSchema, + }), ]), parametersJsonSchema: zodToJsonSchema( z.discriminatedUnion('method', [ @@ -108,15 +149,33 @@ export function createMcpTool(config: McpConfig): Tool { method: z.literal('getResource'), params: getResourceSchema, }), + z.object({ + method: z.literal('listTools'), + params: listToolsSchema.optional(), + }), + z.object({ + method: z.literal('executeTool'), + params: executeToolSchema, + }), ]), ), - returns: z.union([listResourcesReturnSchema, getResourceReturnSchema]), + returns: z.union([ + listResourcesReturnSchema, + getResourceReturnSchema, + listToolsReturnSchema, + executeToolReturnSchema, + ]), returnsJsonSchema: zodToJsonSchema( - z.union([listResourcesReturnSchema, getResourceReturnSchema]), + z.union([ + listResourcesReturnSchema, + getResourceReturnSchema, + listToolsReturnSchema, + executeToolReturnSchema, + ]), ), execute: async ({ method, params }, { logger }) => { - // Extract the server name from a resource URI + // Extract the server name from a URI (resource or tool) function getServerNameFromUri(uri: string): string | undefined { const match = uri.match(/^([^:]+):\/\//); return match ? match[1] : undefined; @@ -180,6 +239,64 @@ export function createMcpTool(config: McpConfig): Tool { logger.verbose(`Fetching resource: ${uri}`); const resource = await client.resource(uri); return resource.content; + } else if (method === 'listTools') { + // List available tools from MCP servers + const tools: 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 tools from server: ${serverFilter}`); + const serverTools = await client.tools(); + tools.push(...(serverTools as any[])); + } catch (error) { + logger.error( + `Failed to fetch tools 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 tools from server: ${serverName}`); + const serverTools = await client.tools(); + tools.push(...(serverTools as any[])); + } catch (error) { + logger.error( + `Failed to fetch tools from server ${serverName}:`, + error, + ); + } + } + } + + return tools; + } else if (method === 'executeTool') { + // Execute a tool from an MCP server + const { uri, params: toolParams = {} } = params; + + // 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 execute the tool + logger.verbose(`Executing tool: ${uri} with params:`, toolParams); + const result = await client.tool(uri, toolParams); + return result; } throw new Error(`Unknown method: ${method}`); @@ -188,20 +305,36 @@ export function createMcpTool(config: McpConfig): Tool { logParameters: (params, { logger }) => { if (params.method === 'listResources') { logger.verbose( - `Listing MCP resources${params.params?.server ? ` from server: ${params.params.server}` : ''}`, + `Listing MCP resources${ + params.params?.server ? ` from server: ${params.params.server}` : '' + }`, ); } else if (params.method === 'getResource') { logger.verbose(`Fetching MCP resource: ${params.params.uri}`); + } else if (params.method === 'listTools') { + logger.verbose( + `Listing MCP tools${ + params.params?.server ? ` from server: ${params.params.server}` : '' + }`, + ); + } else if (params.method === 'executeTool') { + logger.verbose(`Executing MCP tool: ${params.params.uri}`); } }, logReturns: (result, { logger }) => { if (Array.isArray(result)) { - logger.verbose(`Found ${result.length} MCP resources`); - } else { + if (result.length > 0 && 'description' in result[0]) { + logger.verbose(`Found ${result.length} MCP tools`); + } else { + logger.verbose(`Found ${result.length} MCP resources`); + } + } else if (typeof result === 'string') { logger.verbose( `Retrieved MCP resource content (${result.length} characters)`, ); + } else { + logger.verbose(`Executed MCP tool and received result`); } }, }; diff --git a/packages/cli/README.md b/packages/cli/README.md index 3ef5b2e..d99a941 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -204,6 +204,9 @@ export default { // Optional: Default context resources to load defaultResources: ['company-docs://api/reference'], + + // Optional: Default tools to make available + defaultTools: ['company-docs://tools/search'], }, }; ``` @@ -212,6 +215,25 @@ When MCP is configured, the agent will have access to a new `mcp` tool that allo - List available resources from configured MCP servers - Fetch resources to use as context for its work +- List available tools from configured MCP servers +- Execute tools provided by MCP servers + +#### Using MCP Tools + +MCP tools allow the agent to execute functions provided by external services through the Model Context Protocol. The agent can: + +1. Discover available tools using `mcp.listTools()` +2. Execute a tool using `mcp.executeTool({ uri: 'server-name://path/to/tool', params: { ... } })` + +Tools can provide various capabilities like: + +- Searching documentation +- Accessing databases +- Interacting with APIs +- Performing specialized calculations +- Accessing proprietary services + +Each tool has a URI that identifies it, along with parameters it accepts and the type of result it returns. ### CLI-Only Options