From f55016ea37b825b8a2f0cd95501010c644a51eca Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 21:55:46 +0000 Subject: [PATCH 1/5] Add MCP client implementation - Implement Model Context Protocol client based on the development guide - Add client, transport, and capability management - Add context formatting and management - Add utility functions for validation, error handling, and logging - Add tests for the MCP client - Add example usage Co-Authored-By: kiet@onlook.dev --- bun.lockb | Bin 656632 -> 669024 bytes packages/ai/package.json | 4 +- packages/ai/src/index.ts | 1 + packages/ai/src/mcp/capabilities/index.ts | 8 + packages/ai/src/mcp/capabilities/prompts.ts | 84 +++++ packages/ai/src/mcp/capabilities/resources.ts | 110 ++++++ packages/ai/src/mcp/capabilities/roots.ts | 48 +++ packages/ai/src/mcp/capabilities/tools.ts | 167 +++++++++ packages/ai/src/mcp/client/client.ts | 351 ++++++++++++++++++ packages/ai/src/mcp/client/index.ts | 7 + packages/ai/src/mcp/client/transport.ts | 57 +++ packages/ai/src/mcp/client/types.ts | 182 +++++++++ packages/ai/src/mcp/context/formatter.ts | 104 ++++++ packages/ai/src/mcp/context/index.ts | 6 + packages/ai/src/mcp/context/manager.ts | 139 +++++++ packages/ai/src/mcp/examples/basic-client.ts | 82 ++++ packages/ai/src/mcp/index.ts | 12 + packages/ai/src/mcp/utils/error.ts | 93 +++++ packages/ai/src/mcp/utils/index.ts | 7 + packages/ai/src/mcp/utils/logging.ts | 130 +++++++ packages/ai/src/mcp/utils/validation.ts | 69 ++++ packages/ai/test/mcp/client.test.ts | 234 ++++++++++++ 22 files changed, 1894 insertions(+), 1 deletion(-) create mode 100644 packages/ai/src/mcp/capabilities/index.ts create mode 100644 packages/ai/src/mcp/capabilities/prompts.ts create mode 100644 packages/ai/src/mcp/capabilities/resources.ts create mode 100644 packages/ai/src/mcp/capabilities/roots.ts create mode 100644 packages/ai/src/mcp/capabilities/tools.ts create mode 100644 packages/ai/src/mcp/client/client.ts create mode 100644 packages/ai/src/mcp/client/index.ts create mode 100644 packages/ai/src/mcp/client/transport.ts create mode 100644 packages/ai/src/mcp/client/types.ts create mode 100644 packages/ai/src/mcp/context/formatter.ts create mode 100644 packages/ai/src/mcp/context/index.ts create mode 100644 packages/ai/src/mcp/context/manager.ts create mode 100644 packages/ai/src/mcp/examples/basic-client.ts create mode 100644 packages/ai/src/mcp/index.ts create mode 100644 packages/ai/src/mcp/utils/error.ts create mode 100644 packages/ai/src/mcp/utils/index.ts create mode 100644 packages/ai/src/mcp/utils/logging.ts create mode 100644 packages/ai/src/mcp/utils/validation.ts create mode 100644 packages/ai/test/mcp/client.test.ts diff --git a/bun.lockb b/bun.lockb index 6b3679a839064402d5f28f2ee300a1fffed941f6..13eb0c23e28bf31a1b14042d2b51482dd22e0440 100755 GIT binary patch delta 122282 zcmcfq2Ygk<_WzCV6F6`-hzdwkX)0)t072vg1BoIcB6h`wN=O2nkOE1lmJ_gGSKQ)O zv0%mEMMbQrT&&nTb`-loQSsWaKJU-W?32Kc>vNy~|9L&<>)|t>HEqqBHEU+iKDpmt zFW77ShP^KDmpvu-o39QW_4=->$Ne(s?}s|>yeQS~fp>adyWLf7uiSOXA3Oi_>Y!Gh zKEoFr-~Hs3{aYAf|JlLw>LyRGnKB!CaLn^s!(Rlq1KVX;J{A<8T{L}8Ipscq`|zoy zl_hHBEtmhqg4epIvzmKe8!}5uCrqD2W<`yPcwQcjsfO4%EnktUOieASsreSJZY~8of|2}OCvUf-Rc{iA zsO}6WSEGp1b^qaCb@UAaHS}*#JFn@Zj@lV{n#4T_P^PZBw5FH853bNXbn^ME6njGVA)Aq0x-lmp1co|griqh&yr2{=L zk{9-}Q#G-urgj?jt4pWX7#(|C-+vsG54G9TPE~(bzNEb64E5(d70hnlJ$>L_RkFyJ?}o$kY`-M(qa^y z)z#LNr?gr}x_U{6eL>mr!vVJZ;9Oh2be1AVa8-Qg^ia&Iv6R*j4kmiu3b>YMABxI^ zYpJUI;);?fvc=o`Snpm1%DdMNvSWJ=RCye*cJ|a#@3MXE&@OFheWqxz=i$e7|Dql% zxvoRf^Li>CJcKb7Nbbt>Jf^L#V2J1S055@4pl;XwJP$vriyDYJjvD@&hGgVvlvf91 zK`pleurs(PsO8ueR6l>Y@?Q;;ZHNbx3hZpXgFv(NB&g+iOrah5zu;1UWe#&edCRe2N5z8&540V(A7M?C1#BYXc`9_wR4o|D%X|CbCLu`Y4fwII1 z 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�~+Isv6teu0_4s8RPztsZKTBB{$ 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 delta 117128 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 diff --git a/packages/ai/package.json b/packages/ai/package.json index f1e268b76d..37e086c3d3 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -31,8 +31,10 @@ "@onlook/typescript": "*" }, "dependencies": { + "@modelcontextprotocol/sdk": "latest", "diff-match-patch": "^1.0.5", "fg": "^0.0.3", - "marked": "^15.0.7" + "marked": "^15.0.7", + "zod": "^3.22.4" } } diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 7bfe02e857..2fd23f6a0f 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,2 +1,3 @@ export * from './coder'; export * from './prompt'; +export * from './mcp'; diff --git a/packages/ai/src/mcp/capabilities/index.ts b/packages/ai/src/mcp/capabilities/index.ts new file mode 100644 index 0000000000..e1b652418d --- /dev/null +++ b/packages/ai/src/mcp/capabilities/index.ts @@ -0,0 +1,8 @@ +/** + * MCP capability exports + */ + +export * from './tools'; +export * from './resources'; +export * from './prompts'; +export * from './roots'; diff --git a/packages/ai/src/mcp/capabilities/prompts.ts b/packages/ai/src/mcp/capabilities/prompts.ts new file mode 100644 index 0000000000..55bad6596e --- /dev/null +++ b/packages/ai/src/mcp/capabilities/prompts.ts @@ -0,0 +1,84 @@ +/** + * Prompt capability management for MCP client + */ + +import type { Prompt } from '../client/types'; +import { MCPClient } from '../client'; +import { MCPError, MCPErrorType } from '../client/types'; + +/** + * Find a prompt by name + * + * @param client MCP client + * @param name Name of the prompt to find + * @returns Prompt if found, null otherwise + */ +export async function findPromptByName(client: MCPClient, name: string): Promise { + const prompts = await client.listPrompts(); + return prompts.prompts.find((p) => p.name === name) || null; +} + +/** + * Validate prompt arguments + * + * @param prompt Prompt to validate arguments for + * @param args Arguments to validate + * @returns Validated arguments + */ +export function validatePromptArgs( + prompt: Prompt, + args: Record, +): Record { + if (!prompt.arguments || prompt.arguments.length === 0) { + return {}; + } + + const validatedArgs: Record = {}; + const missingRequired: string[] = []; + + for (const arg of prompt.arguments) { + const value = args[arg.name]; + + if (arg.required && (value === undefined || value === null)) { + missingRequired.push(arg.name); + } else if (value !== undefined) { + validatedArgs[arg.name] = value; + } + } + + if (missingRequired.length > 0) { + throw new MCPError( + `Missing required arguments for prompt ${prompt.name}: ${missingRequired.join(', ')}`, + MCPErrorType.VALIDATION_ERROR, + ); + } + + return validatedArgs; +} + +/** + * Get a prompt with validated arguments + * + * @param client MCP client + * @param promptName Name of the prompt to get + * @param args Arguments for the prompt + * @returns Prompt with validated arguments + */ +export async function getPromptWithValidation( + client: MCPClient, + promptName: string, + args: Record, +) { + const prompt = await findPromptByName(client, promptName); + + if (!prompt) { + throw new MCPError(`Prompt not found: ${promptName}`, MCPErrorType.PROMPT_ERROR); + } + + const validatedArgs = validatePromptArgs(prompt, args); + + return { + prompt, + args: validatedArgs, + }; +} diff --git a/packages/ai/src/mcp/capabilities/resources.ts b/packages/ai/src/mcp/capabilities/resources.ts new file mode 100644 index 0000000000..23369dc11a --- /dev/null +++ b/packages/ai/src/mcp/capabilities/resources.ts @@ -0,0 +1,110 @@ +/** + * Resource capability management for MCP client + */ + +import type { Resource, ResourceTemplate } from '../client/types'; +import { MCPClient } from '../client'; +import { MCPError, MCPErrorType } from '../client/types'; + +/** + * Expand a resource template with parameters + * + * @param template Resource template to expand + * @param params Parameters to expand the template with + * @returns Expanded URI + */ +export function expandResourceTemplate( + template: ResourceTemplate, + params: Record, +): string { + let uri = template.uriTemplate; + + for (const [key, value] of Object.entries(params)) { + uri = uri.replace(`{${key}}`, value); + } + + return uri; +} + +/** + * Read a resource with template expansion + * + * @param client MCP client + * @param uriOrTemplate URI or template to read + * @param params Parameters for template expansion + * @returns Resource content + */ +export async function readResourceWithTemplate( + client: MCPClient, + uriOrTemplate: string, + params?: Record, +) { + try { + // If params are provided, treat as a template + if (params) { + const resources = await client.listResources(); + // The SDK types don't match our types exactly + const resourceTemplates = Array.isArray(resources.resourceTemplates) + ? resources.resourceTemplates + : []; + const template = resourceTemplates.find((t: any) => t.uriTemplate === uriOrTemplate); + + if (!template) { + throw new MCPError( + `Resource template not found: ${uriOrTemplate}`, + MCPErrorType.RESOURCE_ERROR, + ); + } + + const uri = expandResourceTemplate(template, params); + return await client.readResource(uri); + } + + // Otherwise, treat as a direct URI + return await client.readResource(uriOrTemplate); + } catch (error) { + if (error instanceof MCPError) { + throw error; + } + + throw new MCPError( + `Failed to read resource: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.RESOURCE_ERROR, + error, + ); + } +} + +/** + * Find a resource by name + * + * @param client MCP client + * @param name Name of the resource to find + * @returns Resource if found, null otherwise + */ +export async function findResourceByName( + client: MCPClient, + name: string, +): Promise { + const resources = await client.listResources(); + return resources.resources.find((r) => r.name === name) || null; +} + +/** + * Find a resource template by name + * + * @param client MCP client + * @param name Name of the resource template to find + * @returns Resource template if found, null otherwise + */ +export async function findResourceTemplateByName( + client: MCPClient, + name: string, +): Promise { + const resources = await client.listResources(); + // The SDK types don't match our types exactly + const resourceTemplates = Array.isArray(resources.resourceTemplates) + ? resources.resourceTemplates + : []; + return resourceTemplates.find((t: any) => t.name === name) || null; +} diff --git a/packages/ai/src/mcp/capabilities/roots.ts b/packages/ai/src/mcp/capabilities/roots.ts new file mode 100644 index 0000000000..94ee0bc922 --- /dev/null +++ b/packages/ai/src/mcp/capabilities/roots.ts @@ -0,0 +1,48 @@ +/** + * Root capability management for MCP client + */ + +import type { Root } from '../client/types'; + +/** + * Create a file system root + * + * @param path Path to the root directory + * @param name Name of the root + * @returns Root declaration + */ +export function createFileSystemRoot(path: string, name: string): Root { + return { + uri: `file://${path}`, + name, + }; +} + +/** + * Create an HTTP root + * + * @param url URL of the root + * @param name Name of the root + * @returns Root declaration + */ +export function createHttpRoot(url: string, name: string): Root { + return { + uri: url.startsWith('http') ? url : `https://${url}`, + name, + }; +} + +/** + * Create a set of common roots for a project + * + * @param projectPath Path to the project directory + * @param projectName Name of the project + * @returns Array of root declarations + */ +export function createProjectRoots(projectPath: string, projectName: string): Root[] { + return [ + createFileSystemRoot(projectPath, projectName), + createFileSystemRoot(`${projectPath}/src`, `${projectName} Source`), + createFileSystemRoot(`${projectPath}/test`, `${projectName} Tests`), + ]; +} diff --git a/packages/ai/src/mcp/capabilities/tools.ts b/packages/ai/src/mcp/capabilities/tools.ts new file mode 100644 index 0000000000..62e1b12a20 --- /dev/null +++ b/packages/ai/src/mcp/capabilities/tools.ts @@ -0,0 +1,167 @@ +/** + * Tool capability management for MCP client + */ + +import { z } from 'zod'; +import type { Tool } from '../client/types'; +import { MCPClient } from '../client'; +import { MCPError, MCPErrorType } from '../client/types'; + +/** + * Validate tool arguments against the tool's input schema + * + * @param tool Tool to validate arguments for + * @param args Arguments to validate + * @returns Validated arguments + */ +export function validateToolArgs( + tool: Tool, + args: Record, +): Record { + try { + const schema = createZodSchema(tool.inputSchema); + return schema.parse(args); + } catch (error) { + throw new MCPError( + `Invalid arguments for tool ${tool.name}: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.VALIDATION_ERROR, + error, + ); + } +} + +/** + * Create a Zod schema from a JSON schema + * + * @param schema JSON schema + * @returns Zod schema + */ +export function createZodSchema(schema: any): z.ZodTypeAny { + if (!schema) { + return z.any(); + } + + const type = schema.type; + + if (type === 'string') { + let stringSchema = z.string(); + if (schema.pattern) { + stringSchema = stringSchema.regex(new RegExp(schema.pattern)); + } + if (schema.minLength !== undefined) { + stringSchema = stringSchema.min(schema.minLength); + } + if (schema.maxLength !== undefined) { + stringSchema = stringSchema.max(schema.maxLength); + } + return stringSchema; + } else if (type === 'number') { + let numberSchema = z.number(); + if (schema.minimum !== undefined) { + numberSchema = numberSchema.min(schema.minimum); + } + if (schema.maximum !== undefined) { + numberSchema = numberSchema.max(schema.maximum); + } + return numberSchema; + } else if (type === 'integer') { + let intSchema = z.number().int(); + if (schema.minimum !== undefined) { + intSchema = intSchema.min(schema.minimum); + } + if (schema.maximum !== undefined) { + intSchema = intSchema.max(schema.maximum); + } + return intSchema; + } else if (type === 'boolean') { + return z.boolean(); + } else if (type === 'array') { + const items = schema.items || {}; + let arraySchema = z.array(createZodSchema(items)); + if (schema.minItems !== undefined) { + arraySchema = arraySchema.min(schema.minItems); + } + if (schema.maxItems !== undefined) { + arraySchema = arraySchema.max(schema.maxItems); + } + return arraySchema; + } else if (type === 'object') { + const properties = schema.properties || {}; + const shape: Record = {}; + + for (const [key, value] of Object.entries(properties)) { + shape[key] = createZodSchema(value); + } + + let objectSchema = z.object(shape); + + if (schema.required && Array.isArray(schema.required)) { + const required = schema.required as string[]; + for (const key of required) { + if (shape[key]) { + shape[key] = shape[key].optional(); + } + } + } + + return objectSchema; + } else { + return z.any(); + } +} + +/** + * Call a tool with validated arguments + * + * @param client MCP client + * @param toolName Name of the tool to call + * @param args Arguments for the tool + * @returns Result of the tool call + */ +export async function callToolWithValidation( + client: MCPClient, + toolName: string, + args: Record, +) { + const tools = await client.listTools(); + const tool = tools.tools.find((t) => t.name === toolName); + + if (!tool) { + throw new MCPError(`Tool not found: ${toolName}`, MCPErrorType.TOOL_CALL_ERROR); + } + + // @ts-ignore - The SDK types don't match our types exactly + const validatedArgs = validateToolArgs(tool, args); + return await client.callTool(tool.name, validatedArgs); +} + +/** + * Generate an example for a tool + * + * @param tool Tool to generate an example for + * @returns Example arguments for the tool + */ +export function generateToolExample(tool: Tool): Record | null { + if (!tool.inputSchema || !tool.inputSchema.properties) { + return null; + } + + const exampleArgs: Record = {}; + + for (const [propName, prop] of Object.entries(tool.inputSchema.properties)) { + const typedProp = prop as { type: string }; + if (typedProp.type === 'string') { + exampleArgs[propName] = 'example_string'; + } else if (typedProp.type === 'number' || typedProp.type === 'integer') { + exampleArgs[propName] = 42; + } else if (typedProp.type === 'boolean') { + exampleArgs[propName] = true; + } else if (typedProp.type === 'array') { + exampleArgs[propName] = []; + } else if (typedProp.type === 'object') { + exampleArgs[propName] = {}; + } + } + + return exampleArgs; +} diff --git a/packages/ai/src/mcp/client/client.ts b/packages/ai/src/mcp/client/client.ts new file mode 100644 index 0000000000..575677f520 --- /dev/null +++ b/packages/ai/src/mcp/client/client.ts @@ -0,0 +1,351 @@ +/** + * Core MCP client implementation + */ + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import { createTransport } from './transport'; +import type { + MCPClientOptions, + Root, + ServerCapabilities, + ToolCallResult, + TransportOptions, +} from './types'; +import { MCPError, MCPErrorType } from './types'; + +/** + * MCP client for communicating with MCP servers + */ +export class MCPClient { + private client: Client; + private connected: boolean = false; + private initialized: boolean = false; + private serverCapabilities: ServerCapabilities | null = null; + + /** + * Create a new MCP client + * + * @param options Client options + */ + constructor(options: MCPClientOptions = {}) { + this.client = new Client( + { + name: options.name || 'onlook-mcp-client', + version: options.version || '1.0.0', + }, + {}, + ); + } + + /** + * Connect to an MCP server + * + * @param transportOptions Transport options + */ + async connect(transportOptions: TransportOptions): Promise { + try { + const transport = await createTransport(transportOptions); + await this.connectWithTransport(transport); + } catch (error) { + if (error instanceof MCPError) { + throw error; + } + throw new MCPError( + `Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.CONNECTION_ERROR, + error, + ); + } + } + + /** + * Connect to an MCP server with a transport + * + * @param transport Transport instance + */ + async connectWithTransport(transport: Transport): Promise { + try { + await this.client.connect(transport); + this.connected = true; + } catch (error) { + throw new MCPError( + `Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.CONNECTION_ERROR, + error, + ); + } + } + + /** + * Initialize the MCP client + * + * @param roots Optional roots to declare to the server + * @returns Server capabilities + */ + async initialize(roots?: Root[]): Promise { + if (!this.connected) { + throw new MCPError( + 'Client is not connected to a server', + MCPErrorType.INITIALIZATION_ERROR, + ); + } + + try { + // @ts-ignore - The SDK types don't match our types exactly + const response = await this.client.initialize({ + roots, + }); + + this.serverCapabilities = response as ServerCapabilities; + this.initialized = true; + return response as ServerCapabilities; + } catch (error) { + throw new MCPError( + `Failed to initialize MCP client: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.INITIALIZATION_ERROR, + error, + ); + } + } + + /** + * List available tools from the server + * + * @returns List of available tools + */ + async listTools() { + this.ensureInitialized(); + + try { + // @ts-ignore - The SDK types don't match our types exactly + return await this.client.listTools(); + } catch (error) { + throw new MCPError( + `Failed to list tools: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.TOOL_CALL_ERROR, + error, + ); + } + } + + /** + * List available prompts from the server + * + * @returns List of available prompts + */ + async listPrompts() { + this.ensureInitialized(); + + try { + // @ts-ignore - The SDK types don't match our types exactly + return await this.client.listPrompts(); + } catch (error) { + throw new MCPError( + `Failed to list prompts: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.PROMPT_ERROR, + error, + ); + } + } + + /** + * List available resources from the server + * + * @returns List of available resources + */ + async listResources() { + this.ensureInitialized(); + + try { + // @ts-ignore - The SDK types don't match our types exactly + return await this.client.listResources(); + } catch (error) { + throw new MCPError( + `Failed to list resources: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.RESOURCE_ERROR, + error, + ); + } + } + + /** + * Call a tool on the server + * + * @param name Name of the tool to call + * @param args Arguments for the tool + * @returns Result of the tool call + */ + async callTool(name: string, args: Record): Promise { + this.ensureInitialized(); + + try { + // @ts-ignore - The SDK types don't match our types exactly + const result = await this.client.callTool({ + name, + arguments: args, + }); + return result as ToolCallResult; + } catch (error) { + throw new MCPError( + `Failed to call tool ${name}: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.TOOL_CALL_ERROR, + error, + ); + } + } + + /** + * Read a resource from the server + * + * @param uri URI of the resource to read + * @returns Resource content + */ + async readResource(uri: string) { + this.ensureInitialized(); + + try { + // @ts-ignore - The SDK types don't match our types exactly + return await this.client.readResource({ + uri, + }); + } catch (error) { + throw new MCPError( + `Failed to read resource ${uri}: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.RESOURCE_ERROR, + error, + ); + } + } + + /** + * Subscribe to a resource for updates + * + * @param uri URI of the resource to subscribe to + */ + async subscribeToResource(uri: string) { + this.ensureInitialized(); + + try { + // @ts-ignore - The SDK types don't match our types exactly + await this.client.subscribeResource({ + uri, + }); + } catch (error) { + throw new MCPError( + `Failed to subscribe to resource ${uri}: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.RESOURCE_ERROR, + error, + ); + } + } + + /** + * Unsubscribe from a resource + * + * @param uri URI of the resource to unsubscribe from + */ + async unsubscribeFromResource(uri: string) { + this.ensureInitialized(); + + try { + // @ts-ignore - The SDK types don't match our types exactly + await this.client.unsubscribeResource({ + uri, + }); + } catch (error) { + throw new MCPError( + `Failed to unsubscribe from resource ${uri}: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.RESOURCE_ERROR, + error, + ); + } + } + + /** + * Set a request handler for the client + * + * @param method Method to handle + * @param handler Handler function + */ + setRequestHandler(method: string, handler: (params: T) => Promise) { + // @ts-ignore - The SDK types don't match our types exactly + this.client.setRequestHandler(method, handler); + } + + /** + * Set a notification handler for the client + * + * @param method Method to handle + * @param handler Handler function + */ + setNotificationHandler(method: string, handler: (params: T) => void) { + // @ts-ignore - The SDK types don't match our types exactly + this.client.setNotificationHandler(method, handler); + } + + /** + * Close the connection to the server + */ + async close() { + try { + await this.client.close(); + this.connected = false; + this.initialized = false; + this.serverCapabilities = null; + } catch (error) { + throw new MCPError( + `Failed to close MCP client: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.CONNECTION_ERROR, + error, + ); + } + } + + /** + * Get the underlying client instance + * + * @returns Client instance + */ + getClient(): Client { + return this.client; + } + + /** + * Get the server capabilities + * + * @returns Server capabilities + */ + getServerCapabilities(): ServerCapabilities | null { + return this.serverCapabilities; + } + + /** + * Check if the client is connected + * + * @returns Whether the client is connected + */ + isConnected(): boolean { + return this.connected; + } + + /** + * Check if the client is initialized + * + * @returns Whether the client is initialized + */ + isInitialized(): boolean { + return this.initialized; + } + + /** + * Ensure that the client is initialized + * + * @throws MCPError if the client is not initialized + */ + private ensureInitialized() { + if (!this.initialized) { + throw new MCPError('Client is not initialized', MCPErrorType.INITIALIZATION_ERROR); + } + } +} diff --git a/packages/ai/src/mcp/client/index.ts b/packages/ai/src/mcp/client/index.ts new file mode 100644 index 0000000000..1d2383a314 --- /dev/null +++ b/packages/ai/src/mcp/client/index.ts @@ -0,0 +1,7 @@ +/** + * MCP client exports + */ + +export * from './client'; +export * from './transport'; +export * from './types'; diff --git a/packages/ai/src/mcp/client/transport.ts b/packages/ai/src/mcp/client/transport.ts new file mode 100644 index 0000000000..d1f35e2c7c --- /dev/null +++ b/packages/ai/src/mcp/client/transport.ts @@ -0,0 +1,57 @@ +/** + * Transport implementations for MCP client + */ + +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import type { TransportOptions } from './types'; +import { MCPError, MCPErrorType } from './types'; + +/** + * Create a transport for connecting to an MCP server + * + * @param options Transport options + * @returns Transport instance + */ +export async function createTransport(options: TransportOptions): Promise { + try { + switch (options.type) { + case 'stdio': + if (!options.command) { + throw new MCPError( + 'Command is required for stdio transport', + MCPErrorType.VALIDATION_ERROR, + ); + } + return new StdioClientTransport({ + command: options.command, + args: options.args || [], + }); + case 'websocket': + if (!options.url) { + throw new MCPError( + 'URL is required for websocket transport', + MCPErrorType.VALIDATION_ERROR, + ); + } + throw new MCPError( + 'WebSocket transport is not yet implemented', + MCPErrorType.VALIDATION_ERROR, + ); + default: + throw new MCPError( + `Unsupported transport type: ${(options as any).type}`, + MCPErrorType.VALIDATION_ERROR, + ); + } + } catch (error) { + if (error instanceof MCPError) { + throw error; + } + throw new MCPError( + `Failed to create transport: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.TRANSPORT_ERROR, + error, + ); + } +} diff --git a/packages/ai/src/mcp/client/types.ts b/packages/ai/src/mcp/client/types.ts new file mode 100644 index 0000000000..b323413e06 --- /dev/null +++ b/packages/ai/src/mcp/client/types.ts @@ -0,0 +1,182 @@ +/** + * Type definitions for the MCP client + */ + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; + +/** + * Tool definition + */ +export interface Tool { + name: string; + description: string; + inputSchema: { + type: string; + properties?: Record< + string, + { + type: string; + description?: string; + } + >; + required?: string[]; + }; +} + +/** + * Prompt definition + */ +export interface Prompt { + name: string; + description?: string; + arguments?: Array<{ + name: string; + description?: string; + required?: boolean; + }>; +} + +/** + * Resource definition + */ +export interface Resource { + name: string; + description?: string; + uri: string; +} + +/** + * Resource template definition + */ +export interface ResourceTemplate { + name: string; + description: string; + uriTemplate: string; +} + +/** + * Options for creating an MCP client + */ +export interface MCPClientOptions { + /** + * Name of the client + * @default "onlook-mcp-client" + */ + name?: string; + + /** + * Version of the client + * @default "1.0.0" + */ + version?: string; +} + +/** + * Transport options for connecting to an MCP server + */ +export interface TransportOptions { + /** + * Type of transport to use + */ + type: 'stdio' | 'websocket'; + + /** + * Command to execute for stdio transport + */ + command?: string; + + /** + * Arguments for the command for stdio transport + */ + args?: string[]; + + /** + * URL for websocket transport + */ + url?: string; +} + +/** + * Root declaration for MCP servers + */ +export interface Root { + /** + * URI of the root + */ + uri: string; + + /** + * Name of the root + */ + name: string; +} + +/** + * Server capabilities returned from initialization + */ +export interface ServerCapabilities { + capabilities: { + tools?: Record; + resources?: Record; + prompts?: Record; + roots?: Record; + sampling?: Record; + }; + serverInfo: { + name: string; + version: string; + }; +} + +/** + * Result of a tool call + */ +export interface ToolCallResult { + /** + * Whether the tool call resulted in an error + */ + isError?: boolean; + + /** + * Content of the tool call result + */ + content: Array<{ + type: string; + text: string; + }>; +} + +/** + * MCP error types + */ +export enum MCPErrorType { + CONNECTION_ERROR = 'connection_error', + INITIALIZATION_ERROR = 'initialization_error', + TOOL_CALL_ERROR = 'tool_call_error', + RESOURCE_ERROR = 'resource_error', + PROMPT_ERROR = 'prompt_error', + TRANSPORT_ERROR = 'transport_error', + VALIDATION_ERROR = 'validation_error', + UNKNOWN_ERROR = 'unknown_error', +} + +/** + * MCP error + */ +export class MCPError extends Error { + type: MCPErrorType; + cause?: unknown; + + constructor(message: string, type: MCPErrorType, cause?: unknown) { + super(message); + this.name = 'MCPError'; + this.type = type; + this.cause = cause; + } +} + +/** + * Export SDK types for convenience + */ +export type { Client, Transport }; diff --git a/packages/ai/src/mcp/context/formatter.ts b/packages/ai/src/mcp/context/formatter.ts new file mode 100644 index 0000000000..e661dec61b --- /dev/null +++ b/packages/ai/src/mcp/context/formatter.ts @@ -0,0 +1,104 @@ +/** + * Context formatting for MCP client + */ + +import type { Tool, Prompt, Resource } from '../client/types'; +import { generateToolExample } from '../capabilities'; + +/** + * Format MCP capabilities for LLM consumption + */ +export class LLMContextFormatter { + /** + * Format tools for LLM consumption + * + * @param tools Tools to format + * @param format Format to use + * @returns Formatted tools + */ + static formatTools( + tools: Tool[], + format: 'anthropic' | 'openai' | 'generic' = 'generic', + ): Record { + if (format === 'anthropic') { + return { + tools: tools.map((tool) => ({ + name: tool.name, + description: tool.description, + input_schema: tool.inputSchema, + })), + }; + } else if (format === 'openai') { + return { + functions: tools.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: tool.inputSchema, + })), + }; + } else { + return { + tools: tools.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: tool.inputSchema, + example: generateToolExample(tool), + })), + }; + } + } + + /** + * Format prompts for LLM consumption + * + * @param prompts Prompts to format + * @returns Formatted prompts + */ + static formatPrompts(prompts: Prompt[]): Record { + return { + prompts: prompts.map((prompt) => ({ + name: prompt.name, + description: prompt.description, + arguments: prompt.arguments, + })), + }; + } + + /** + * Format resources for LLM consumption + * + * @param resources Resources to format + * @returns Formatted resources + */ + static formatResources(resources: Resource[]): Record { + return { + resources: resources.map((resource) => ({ + name: resource.name, + description: resource.description, + uri: resource.uri, + })), + }; + } + + /** + * Format all capabilities for LLM consumption + * + * @param capabilities Capabilities to format + * @param format Format to use + * @returns Formatted capabilities + */ + static formatAllCapabilities( + capabilities: { + tools: Tool[]; + prompts: Prompt[]; + resources: Resource[]; + }, + format: 'anthropic' | 'openai' | 'generic' = 'generic', + ): Record { + return { + ...this.formatTools(capabilities.tools, format), + ...this.formatPrompts(capabilities.prompts), + ...this.formatResources(capabilities.resources), + }; + } +} diff --git a/packages/ai/src/mcp/context/index.ts b/packages/ai/src/mcp/context/index.ts new file mode 100644 index 0000000000..4bf7eef67b --- /dev/null +++ b/packages/ai/src/mcp/context/index.ts @@ -0,0 +1,6 @@ +/** + * MCP context exports + */ + +export * from './manager'; +export * from './formatter'; diff --git a/packages/ai/src/mcp/context/manager.ts b/packages/ai/src/mcp/context/manager.ts new file mode 100644 index 0000000000..2e8662d741 --- /dev/null +++ b/packages/ai/src/mcp/context/manager.ts @@ -0,0 +1,139 @@ +/** + * Context management for MCP client + */ + +import type { Tool, Prompt, Resource } from '../client/types'; +import { MCPClient } from '../client'; +import { generateToolExample } from '../capabilities'; + +/** + * Manager for MCP capabilities and context + */ +export class MCPCapabilityManager { + private client: MCPClient; + private capabilities: { + tools?: Tool[]; + prompts?: Prompt[]; + resources?: Resource[]; + } = {}; + private lastUpdate: number | null = null; + + /** + * Create a new capability manager + * + * @param client MCP client + */ + constructor(client: MCPClient) { + this.client = client; + + // Set up notification handlers for capability changes + client.setNotificationHandler( + 'notifications/tools/list_changed', + this.handleToolsChanged.bind(this), + ); + client.setNotificationHandler( + 'notifications/prompts/list_changed', + this.handlePromptsChanged.bind(this), + ); + client.setNotificationHandler( + 'notifications/resources/list_changed', + this.handleResourcesChanged.bind(this), + ); + } + + /** + * Refresh all capabilities + */ + async refreshAll(): Promise { + await this.refreshTools(); + await this.refreshPrompts(); + await this.refreshResources(); + this.lastUpdate = Date.now(); + } + + /** + * Refresh tools + */ + async refreshTools(): Promise { + try { + const tools = await this.client.listTools(); + // @ts-ignore - The SDK types don't match our types exactly + this.capabilities.tools = tools.tools; + } catch (error) { + console.error('Error refreshing tools:', error); + } + } + + /** + * Refresh prompts + */ + async refreshPrompts(): Promise { + try { + const prompts = await this.client.listPrompts(); + // @ts-ignore - The SDK types don't match our types exactly + this.capabilities.prompts = prompts.prompts; + } catch (error) { + console.error('Error refreshing prompts:', error); + } + } + + /** + * Refresh resources + */ + async refreshResources(): Promise { + try { + const resources = await this.client.listResources(); + // @ts-ignore - The SDK types don't match our types exactly + this.capabilities.resources = resources.resources; + } catch (error) { + console.error('Error refreshing resources:', error); + } + } + + /** + * Handle tools changed notification + */ + private handleToolsChanged(): void { + this.refreshTools(); + } + + /** + * Handle prompts changed notification + */ + private handlePromptsChanged(): void { + this.refreshPrompts(); + } + + /** + * Handle resources changed notification + */ + private handleResourcesChanged(): void { + this.refreshResources(); + } + + /** + * Get all capabilities + * + * @returns All capabilities + */ + getCapabilities(): { + tools: Tool[]; + prompts: Prompt[]; + resources: Resource[]; + } { + return { + tools: this.capabilities.tools || [], + prompts: this.capabilities.prompts || [], + resources: this.capabilities.resources || [], + }; + } + + /** + * Get the last update time + * + * @returns Last update time in milliseconds since epoch + */ + getLastUpdateTime(): number | null { + return this.lastUpdate; + } +} diff --git a/packages/ai/src/mcp/examples/basic-client.ts b/packages/ai/src/mcp/examples/basic-client.ts new file mode 100644 index 0000000000..2de49ec6aa --- /dev/null +++ b/packages/ai/src/mcp/examples/basic-client.ts @@ -0,0 +1,82 @@ +/** + * Basic example of using the MCP client + */ + +import { MCPClient } from '../client'; +import { MCPCapabilityManager } from '../context'; +import { LLMContextFormatter } from '../context'; +import { createFileSystemRoot } from '../capabilities'; +import { MCPLogger, LogLevel } from '../utils'; + +/** + * Run the example + */ +async function runExample() { + // Create a logger + const logger = new MCPLogger({ + level: LogLevel.DEBUG, + prefix: 'MCP-Example', + }); + + logger.info('Creating MCP client...'); + + // Create a client + const client = new MCPClient({ + name: 'onlook-mcp-example', + version: '1.0.0', + }); + + try { + // Connect to a server + logger.info('Connecting to MCP server...'); + await client.connect({ + type: 'stdio', + command: 'mcp-server', + args: ['--stdio'], + }); + + // Initialize the client + logger.info('Initializing MCP client...'); + const roots = [createFileSystemRoot('/path/to/project', 'Project Root')]; + const capabilities = await client.initialize(roots); + logger.info('Server capabilities:', capabilities); + + // Create a capability manager + logger.info('Creating capability manager...'); + const capabilityManager = new MCPCapabilityManager(client); + await capabilityManager.refreshAll(); + + // Get capabilities + const allCapabilities = capabilityManager.getCapabilities(); + logger.info('Available tools:', allCapabilities.tools.length); + logger.info('Available prompts:', allCapabilities.prompts.length); + logger.info('Available resources:', allCapabilities.resources.length); + + // Format capabilities for LLM + logger.info('Formatting capabilities for LLM...'); + const formattedCapabilities = LLMContextFormatter.formatAllCapabilities( + allCapabilities, + 'anthropic', + ); + logger.info('Formatted capabilities:', formattedCapabilities); + + // Call a tool + if (allCapabilities.tools.length > 0) { + const tool = allCapabilities.tools[0]; + logger.info(`Calling tool ${tool.name}...`); + const result = await client.callTool(tool.name, {}); + logger.info('Tool result:', result); + } + + // Close the client + logger.info('Closing MCP client...'); + await client.close(); + } catch (error) { + logger.error('Error:', error); + } +} + +// Run the example if this file is executed directly +if (typeof import.meta.main === 'boolean' && import.meta.main) { + runExample().catch(console.error); +} diff --git a/packages/ai/src/mcp/index.ts b/packages/ai/src/mcp/index.ts new file mode 100644 index 0000000000..de84e64d71 --- /dev/null +++ b/packages/ai/src/mcp/index.ts @@ -0,0 +1,12 @@ +/** + * Model Context Protocol (MCP) client implementation for Onlook + * + * This module provides a client implementation for the Model Context Protocol, + * enabling Onlook to communicate with MCP servers to access tools, resources, + * and prompts through a standardized protocol. + */ + +export * from './client'; +export * from './capabilities'; +export * from './context'; +export * from './utils'; diff --git a/packages/ai/src/mcp/utils/error.ts b/packages/ai/src/mcp/utils/error.ts new file mode 100644 index 0000000000..af92b0a1a9 --- /dev/null +++ b/packages/ai/src/mcp/utils/error.ts @@ -0,0 +1,93 @@ +/** + * Error utilities for MCP client + */ + +import { MCPError, MCPErrorType } from '../client/types'; + +/** + * Create a connection error + * + * @param message Error message + * @param cause Error cause + * @returns MCP error + */ +export function createConnectionError(message: string, cause?: unknown): MCPError { + return new MCPError(message, MCPErrorType.CONNECTION_ERROR, cause); +} + +/** + * Create an initialization error + * + * @param message Error message + * @param cause Error cause + * @returns MCP error + */ +export function createInitializationError(message: string, cause?: unknown): MCPError { + return new MCPError(message, MCPErrorType.INITIALIZATION_ERROR, cause); +} + +/** + * Create a tool call error + * + * @param message Error message + * @param cause Error cause + * @returns MCP error + */ +export function createToolCallError(message: string, cause?: unknown): MCPError { + return new MCPError(message, MCPErrorType.TOOL_CALL_ERROR, cause); +} + +/** + * Create a resource error + * + * @param message Error message + * @param cause Error cause + * @returns MCP error + */ +export function createResourceError(message: string, cause?: unknown): MCPError { + return new MCPError(message, MCPErrorType.RESOURCE_ERROR, cause); +} + +/** + * Create a prompt error + * + * @param message Error message + * @param cause Error cause + * @returns MCP error + */ +export function createPromptError(message: string, cause?: unknown): MCPError { + return new MCPError(message, MCPErrorType.PROMPT_ERROR, cause); +} + +/** + * Create a transport error + * + * @param message Error message + * @param cause Error cause + * @returns MCP error + */ +export function createTransportError(message: string, cause?: unknown): MCPError { + return new MCPError(message, MCPErrorType.TRANSPORT_ERROR, cause); +} + +/** + * Create a validation error + * + * @param message Error message + * @param cause Error cause + * @returns MCP error + */ +export function createValidationError(message: string, cause?: unknown): MCPError { + return new MCPError(message, MCPErrorType.VALIDATION_ERROR, cause); +} + +/** + * Create an unknown error + * + * @param message Error message + * @param cause Error cause + * @returns MCP error + */ +export function createUnknownError(message: string, cause?: unknown): MCPError { + return new MCPError(message, MCPErrorType.UNKNOWN_ERROR, cause); +} diff --git a/packages/ai/src/mcp/utils/index.ts b/packages/ai/src/mcp/utils/index.ts new file mode 100644 index 0000000000..5b4d37f0c5 --- /dev/null +++ b/packages/ai/src/mcp/utils/index.ts @@ -0,0 +1,7 @@ +/** + * MCP utility exports + */ + +export * from './validation'; +export * from './error'; +export * from './logging'; diff --git a/packages/ai/src/mcp/utils/logging.ts b/packages/ai/src/mcp/utils/logging.ts new file mode 100644 index 0000000000..672cd9ceef --- /dev/null +++ b/packages/ai/src/mcp/utils/logging.ts @@ -0,0 +1,130 @@ +/** + * Logging utilities for MCP client + */ + +/** + * Log levels + */ +export enum LogLevel { + DEBUG = 'debug', + INFO = 'info', + WARN = 'warn', + ERROR = 'error', +} + +/** + * Logger for MCP client + */ +export class MCPLogger { + private level: LogLevel; + private prefix: string; + + /** + * Create a new logger + * + * @param options Logger options + */ + constructor( + options: { + level?: LogLevel; + prefix?: string; + } = {}, + ) { + this.level = options.level || LogLevel.INFO; + this.prefix = options.prefix || 'MCP'; + } + + /** + * Log a debug message + * + * @param message Message to log + * @param args Additional arguments + */ + debug(message: string, ...args: unknown[]): void { + if (this.shouldLog(LogLevel.DEBUG)) { + console.debug(`[${this.prefix}] ${message}`, ...args); + } + } + + /** + * Log an info message + * + * @param message Message to log + * @param args Additional arguments + */ + info(message: string, ...args: unknown[]): void { + if (this.shouldLog(LogLevel.INFO)) { + console.info(`[${this.prefix}] ${message}`, ...args); + } + } + + /** + * Log a warning message + * + * @param message Message to log + * @param args Additional arguments + */ + warn(message: string, ...args: unknown[]): void { + if (this.shouldLog(LogLevel.WARN)) { + console.warn(`[${this.prefix}] ${message}`, ...args); + } + } + + /** + * Log an error message + * + * @param message Message to log + * @param args Additional arguments + */ + error(message: string, ...args: unknown[]): void { + if (this.shouldLog(LogLevel.ERROR)) { + console.error(`[${this.prefix}] ${message}`, ...args); + } + } + + /** + * Set the log level + * + * @param level Log level + */ + setLevel(level: LogLevel): void { + this.level = level; + } + + /** + * Set the prefix + * + * @param prefix Prefix + */ + setPrefix(prefix: string): void { + this.prefix = prefix; + } + + /** + * Check if a message should be logged + * + * @param level Log level + * @returns Whether the message should be logged + */ + private shouldLog(level: LogLevel): boolean { + const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]; + const currentLevelIndex = levels.indexOf(this.level); + const messageLevelIndex = levels.indexOf(level); + return messageLevelIndex >= currentLevelIndex; + } +} + +/** + * Create a new logger + * + * @param options Logger options + * @returns Logger + */ +export function createLogger( + options: { + level?: LogLevel; + prefix?: string; + } = {}, +): MCPLogger { + return new MCPLogger(options); +} diff --git a/packages/ai/src/mcp/utils/validation.ts b/packages/ai/src/mcp/utils/validation.ts new file mode 100644 index 0000000000..d01148a97a --- /dev/null +++ b/packages/ai/src/mcp/utils/validation.ts @@ -0,0 +1,69 @@ +/** + * Validation utilities for MCP client + */ + +import { z } from 'zod'; +import { MCPError, MCPErrorType } from '../client/types'; + +/** + * Validate a value against a schema + * + * @param schema Schema to validate against + * @param value Value to validate + * @param errorMessage Error message to use if validation fails + * @returns Validated value + */ +export function validate(schema: z.ZodType, value: unknown, errorMessage: string): T { + try { + return schema.parse(value); + } catch (error) { + throw new MCPError( + `${errorMessage}: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.VALIDATION_ERROR, + error, + ); + } +} + +/** + * Create a schema for transport options + * + * @returns Schema for transport options + */ +export function createTransportOptionsSchema() { + return z.discriminatedUnion('type', [ + z.object({ + type: z.literal('stdio'), + command: z.string(), + args: z.array(z.string()).optional(), + }), + z.object({ + type: z.literal('websocket'), + url: z.string().url(), + }), + ]); +} + +/** + * Create a schema for client options + * + * @returns Schema for client options + */ +export function createClientOptionsSchema() { + return z.object({ + name: z.string().optional(), + version: z.string().optional(), + }); +} + +/** + * Create a schema for root declaration + * + * @returns Schema for root declaration + */ +export function createRootSchema() { + return z.object({ + uri: z.string(), + name: z.string(), + }); +} diff --git a/packages/ai/test/mcp/client.test.ts b/packages/ai/test/mcp/client.test.ts new file mode 100644 index 0000000000..e2e2bfd64d --- /dev/null +++ b/packages/ai/test/mcp/client.test.ts @@ -0,0 +1,234 @@ +/** + * Tests for the MCP client + */ + +import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; +import { MCPClient } from '../../src/mcp/client'; +import { MCPError, MCPErrorType } from '../../src/mcp/client/types'; + +// Mock the transport +const mockTransport = { + connect: mock(() => Promise.resolve()), + send: mock(() => Promise.resolve()), + close: mock(() => Promise.resolve()), + onMessage: mock(() => {}), +}; + +// Mock the client +mock.module('@modelcontextprotocol/sdk/client/index.js', () => ({ + Client: class MockClient { + constructor() {} + connect = mock(() => Promise.resolve()); + initialize = mock(() => + Promise.resolve({ + capabilities: { + tools: {}, + resources: {}, + prompts: {}, + }, + serverInfo: { + name: 'mock-server', + version: '1.0.0', + }, + }), + ); + listTools = mock(() => + Promise.resolve({ + tools: [ + { + name: 'mock-tool', + description: 'A mock tool', + inputSchema: { + type: 'object', + properties: { + input: { + type: 'string', + }, + }, + }, + }, + ], + }), + ); + listPrompts = mock(() => + Promise.resolve({ + prompts: [ + { + name: 'mock-prompt', + description: 'A mock prompt', + arguments: [ + { + name: 'input', + description: 'Input for the prompt', + required: true, + }, + ], + }, + ], + }), + ); + listResources = mock(() => + Promise.resolve({ + resources: [ + { + name: 'mock-resource', + description: 'A mock resource', + uri: 'mock://resource', + }, + ], + resourceTemplates: [ + { + name: 'mock-template', + description: 'A mock template', + uriTemplate: 'mock://template/{param}', + }, + ], + }), + ); + callTool = mock(() => + Promise.resolve({ + content: [ + { + type: 'text', + text: 'Mock tool result', + }, + ], + }), + ); + readResource = mock(() => + Promise.resolve({ + content: 'Mock resource content', + }), + ); + subscribeToResource = mock(() => Promise.resolve()); + unsubscribeFromResource = mock(() => Promise.resolve()); + setRequestHandler = mock(() => {}); + setNotificationHandler = mock(() => {}); + close = mock(() => Promise.resolve()); + }, +})); + +// Mock the transport creation +mock.module('../../src/mcp/client/transport', () => ({ + createTransport: mock(() => Promise.resolve(mockTransport)), +})); + +describe('MCPClient', () => { + let client: MCPClient; + + beforeEach(() => { + client = new MCPClient({ + name: 'test-client', + version: '1.0.0', + }); + }); + + afterEach(() => { + mock.restore(); + }); + + test('should create a client with default options', () => { + const defaultClient = new MCPClient(); + expect(defaultClient).toBeDefined(); + }); + + test('should connect to a server', async () => { + await client.connect({ + type: 'stdio', + command: 'mock-command', + }); + expect(client.isConnected()).toBe(true); + }); + + test('should initialize the client', async () => { + await client.connect({ + type: 'stdio', + command: 'mock-command', + }); + const capabilities = await client.initialize(); + expect(client.isInitialized()).toBe(true); + expect(capabilities).toBeDefined(); + expect(capabilities.serverInfo.name).toBe('mock-server'); + }); + + test('should list tools', async () => { + await client.connect({ + type: 'stdio', + command: 'mock-command', + }); + await client.initialize(); + const tools = await client.listTools(); + expect(tools).toBeDefined(); + expect(tools.tools.length).toBe(1); + expect(tools.tools[0].name).toBe('mock-tool'); + }); + + test('should list prompts', async () => { + await client.connect({ + type: 'stdio', + command: 'mock-command', + }); + await client.initialize(); + const prompts = await client.listPrompts(); + expect(prompts).toBeDefined(); + expect(prompts.prompts.length).toBe(1); + expect(prompts.prompts[0].name).toBe('mock-prompt'); + }); + + test('should list resources', async () => { + await client.connect({ + type: 'stdio', + command: 'mock-command', + }); + await client.initialize(); + const resources = await client.listResources(); + expect(resources).toBeDefined(); + expect(resources.resources.length).toBe(1); + expect(resources.resources[0].name).toBe('mock-resource'); + }); + + test('should call a tool', async () => { + await client.connect({ + type: 'stdio', + command: 'mock-command', + }); + await client.initialize(); + const result = await client.callTool('mock-tool', { input: 'test' }); + expect(result).toBeDefined(); + expect(result.content[0].text).toBe('Mock tool result'); + }); + + test('should read a resource', async () => { + await client.connect({ + type: 'stdio', + command: 'mock-command', + }); + await client.initialize(); + const result = await client.readResource('mock://resource'); + expect(result).toBeDefined(); + expect(result.content).toBe('Mock resource content'); + }); + + test('should close the client', async () => { + await client.connect({ + type: 'stdio', + command: 'mock-command', + }); + await client.initialize(); + await client.close(); + expect(client.isConnected()).toBe(false); + expect(client.isInitialized()).toBe(false); + }); + + test('should throw an error if not connected', async () => { + await expect(client.initialize()).rejects.toThrow(MCPError); + }); + + test('should throw an error if not initialized', async () => { + await client.connect({ + type: 'stdio', + command: 'mock-command', + }); + await expect(client.listTools()).rejects.toThrow(MCPError); + }); +}); From 243d78bcc977999cbb30ef285e7c07ad1d5b52a2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 8 Mar 2025 00:42:45 +0000 Subject: [PATCH 2/5] Add MCP language model adapter for streamText integration Co-Authored-By: kiet@onlook.dev --- apps/studio/electron/main/chat/llmProvider.ts | 36 +++++- packages/ai/src/mcp/adapters/index.ts | 5 + .../ai/src/mcp/adapters/language-model.ts | 111 ++++++++++++++++++ packages/ai/src/mcp/index.ts | 1 + packages/models/src/llm/index.ts | 8 ++ 5 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 packages/ai/src/mcp/adapters/index.ts create mode 100644 packages/ai/src/mcp/adapters/language-model.ts diff --git a/apps/studio/electron/main/chat/llmProvider.ts b/apps/studio/electron/main/chat/llmProvider.ts index a18c34fcf6..105609bb6c 100644 --- a/apps/studio/electron/main/chat/llmProvider.ts +++ b/apps/studio/electron/main/chat/llmProvider.ts @@ -1,7 +1,7 @@ import { createAnthropic } from '@ai-sdk/anthropic'; import type { StreamRequestType } from '@onlook/models/chat'; import { BASE_PROXY_ROUTE, FUNCTIONS_ROUTE, ProxyRoutes } from '@onlook/models/constants'; -import { CLAUDE_MODELS, LLMProvider } from '@onlook/models/llm'; +import { CLAUDE_MODELS, LLMProvider, MCP_MODELS } from '@onlook/models/llm'; import { type LanguageModelV1 } from 'ai'; import { getRefreshedAuthTokens } from '../auth'; export interface OnlookPayload { @@ -10,12 +10,14 @@ export interface OnlookPayload { export async function initModel( provider: LLMProvider, - model: CLAUDE_MODELS, + model: CLAUDE_MODELS | MCP_MODELS, payload: OnlookPayload, ): Promise { switch (provider) { case LLMProvider.ANTHROPIC: - return await getAnthropicProvider(model, payload); + return await getAnthropicProvider(model as CLAUDE_MODELS, payload); + case LLMProvider.MCP: + return await getMCPProvider(model as MCP_MODELS, payload); default: throw new Error(`Unsupported provider: ${provider}`); } @@ -54,3 +56,31 @@ async function getAnthropicProvider( cacheControl: true, }); } + +async function getMCPProvider(model: MCP_MODELS, payload: OnlookPayload): Promise { + // Import the MCP client and adapter + const { MCPClient, createMCPLanguageModel } = await import('@onlook/ai/mcp'); + + // Create a new MCP client + const client = new MCPClient({ + name: 'onlook-mcp-client', + version: '1.0.0', + }); + + // Connect to the MCP server + // Note: The connection details would need to be configured + await client.connect({ + type: 'stdio', + command: 'mcp-server', // This would need to be configured + args: ['--stdio'], + }); + + // Initialize the client + await client.initialize(); + + // Create a language model adapter that implements the LanguageModelV1 interface + return createMCPLanguageModel({ + client, + model: model.toString(), + }); +} diff --git a/packages/ai/src/mcp/adapters/index.ts b/packages/ai/src/mcp/adapters/index.ts new file mode 100644 index 0000000000..8e281e0bb2 --- /dev/null +++ b/packages/ai/src/mcp/adapters/index.ts @@ -0,0 +1,5 @@ +/** + * MCP adapters exports + */ + +export * from './language-model'; diff --git a/packages/ai/src/mcp/adapters/language-model.ts b/packages/ai/src/mcp/adapters/language-model.ts new file mode 100644 index 0000000000..789a909a54 --- /dev/null +++ b/packages/ai/src/mcp/adapters/language-model.ts @@ -0,0 +1,111 @@ +/** + * MCP language model adapter for the AI SDK + */ + +import type { LanguageModelV1, LanguageModelV1CallOptions } from 'ai'; +import { MCPClient } from '../client'; +import { MCPCapabilityManager } from '../context/manager'; +import { MCPError, MCPErrorType } from '../client/types'; + +/** + * Options for creating an MCP language model adapter + */ +export interface MCPLanguageModelOptions { + /** + * MCP client + */ + client: MCPClient; + + /** + * Capability manager + */ + capabilityManager?: MCPCapabilityManager; + + /** + * Model name + */ + model?: string; +} + +/** + * Create an MCP language model adapter + * + * @param options Options for creating the adapter + * @returns Language model adapter + */ +export function createMCPLanguageModel(options: MCPLanguageModelOptions): LanguageModelV1 { + const { client, capabilityManager, model } = options; + + // Create a capability manager if not provided + const manager = capabilityManager || new MCPCapabilityManager(client); + + return { + id: `mcp-${model || 'default'}`, + provider: 'mcp', + doGenerate: async (options: LanguageModelV1CallOptions) => { + try { + // Refresh capabilities if needed + if (!manager.getCapabilities().tools.length) { + await manager.refreshAll(); + } + + // Find a suitable tool for text generation + const tools = manager.getCapabilities().tools; + const generateTool = tools.find( + (tool) => tool.name === 'generate' || tool.name.includes('generate'), + ); + + if (!generateTool) { + throw new MCPError( + 'No text generation tool found', + MCPErrorType.TOOL_CALL_ERROR, + ); + } + + // Extract parameters from options + const { maxTokens, temperature, topP, stopSequences } = options; + + // Prepare messages for the tool call + const messages = []; + + // Add user content if available + if (options.prompt) { + messages.push({ role: 'user', content: options.prompt }); + } + + // Call the tool with the messages + const result = await client.callTool(generateTool.name, { + messages, + maxTokens, + temperature, + topP, + stopSequences, + }); + + // Extract the text from the result + const text = result.content.map((item) => item.text).join(''); + + return { + text, + toolCalls: undefined, + finishReason: 'stop', + usage: { + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + }, + }; + } catch (error) { + if (error instanceof MCPError) { + throw error; + } + + throw new MCPError( + `Failed to generate text: ${error instanceof Error ? error.message : String(error)}`, + MCPErrorType.TOOL_CALL_ERROR, + error, + ); + } + }, + }; +} diff --git a/packages/ai/src/mcp/index.ts b/packages/ai/src/mcp/index.ts index de84e64d71..2f1d453413 100644 --- a/packages/ai/src/mcp/index.ts +++ b/packages/ai/src/mcp/index.ts @@ -10,3 +10,4 @@ export * from './client'; export * from './capabilities'; export * from './context'; export * from './utils'; +export * from './adapters'; diff --git a/packages/models/src/llm/index.ts b/packages/models/src/llm/index.ts index d6e714b568..56431bac1a 100644 --- a/packages/models/src/llm/index.ts +++ b/packages/models/src/llm/index.ts @@ -1,8 +1,16 @@ export enum LLMProvider { ANTHROPIC = 'anthropic', + OPENAI = 'openai', + OLLAMA = 'ollama', + MCP = 'mcp', // Add MCP as a provider type } export enum CLAUDE_MODELS { SONNET = 'claude-3-7-sonnet-20250219', HAIKU = 'claude-3-5-haiku-20241022', } + +export enum MCP_MODELS { + DEFAULT = 'default', + // Add more MCP models as needed +} From 07994cdfb15bc28cb03c5ec2a1bf248ff3e90ce8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 8 Mar 2025 00:44:39 +0000 Subject: [PATCH 3/5] Update LlmManager to support MCP provider Co-Authored-By: kiet@onlook.dev --- apps/studio/electron/main/chat/index.ts | 27 +++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/apps/studio/electron/main/chat/index.ts b/apps/studio/electron/main/chat/index.ts index 11a17ab70f..4fd1bc3936 100644 --- a/apps/studio/electron/main/chat/index.ts +++ b/apps/studio/electron/main/chat/index.ts @@ -1,6 +1,6 @@ import { PromptProvider } from '@onlook/ai/src/prompt/provider'; import { listFilesTool, readFileTool } from '@onlook/ai/src/tools'; -import { CLAUDE_MODELS, LLMProvider } from '@onlook/models'; +import { CLAUDE_MODELS, LLMProvider, MCP_MODELS } from '@onlook/models'; import { ChatSuggestionSchema, StreamRequestType, @@ -9,7 +9,13 @@ import { type UsageCheckResult, } from '@onlook/models/chat'; import { MainChannels } from '@onlook/models/constants'; -import { generateObject, streamText, type CoreMessage, type CoreSystemMessage } from 'ai'; +import { + generateObject, + streamText, + type CoreMessage, + type CoreSystemMessage, + type LanguageModelV1, +} from 'ai'; import { mainWindow } from '..'; import { PersistentStorage } from '../storage'; import { initModel } from './llmProvider'; @@ -68,9 +74,7 @@ class LlmManager { } as CoreSystemMessage; messages = [systemMessage, ...messages]; } - const model = await initModel(LLMProvider.ANTHROPIC, CLAUDE_MODELS.SONNET, { - requestType, - }); + const model = await this.getModel(requestType); const { textStream } = await streamText({ model, @@ -156,11 +160,18 @@ class LlmManager { return 'An unknown error occurred'; } + private async getModel(requestType: StreamRequestType): Promise { + // Get the provider and model from settings or use defaults + const settings = PersistentStorage.USER_SETTINGS.read() || {}; + const provider = settings.llmProvider || LLMProvider.ANTHROPIC; + const modelName = settings.llmModel || CLAUDE_MODELS.SONNET; + + return await initModel(provider, modelName, { requestType }); + } + public async generateSuggestions(messages: CoreMessage[]): Promise { try { - const model = await initModel(LLMProvider.ANTHROPIC, CLAUDE_MODELS.HAIKU, { - requestType: StreamRequestType.SUGGESTIONS, - }); + const model = await this.getModel(StreamRequestType.SUGGESTIONS); const { object } = await generateObject({ model, From 78af486142302a693c5e22ecbeea6cc0e52ef362 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 8 Mar 2025 00:45:40 +0000 Subject: [PATCH 4/5] Update UserSettings interface to include LLM provider and model settings Co-Authored-By: kiet@onlook.dev --- packages/models/src/settings/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/models/src/settings/index.ts b/packages/models/src/settings/index.ts index de454ad987..048539ddb1 100644 --- a/packages/models/src/settings/index.ts +++ b/packages/models/src/settings/index.ts @@ -7,6 +7,8 @@ export interface UserSettings { signInMethod?: string; editor?: EditorSettings; chat?: ChatSettings; + llmProvider?: string; + llmModel?: string; } export interface EditorSettings { From 2337e3e5bcb1f4ef9848f2c9074c2aaab777cb7e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 8 Mar 2025 00:48:21 +0000 Subject: [PATCH 5/5] Fix MCP language model adapter to match LanguageModelV1 interface Co-Authored-By: kiet@onlook.dev --- .../ai/src/mcp/adapters/language-model.ts | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/mcp/adapters/language-model.ts b/packages/ai/src/mcp/adapters/language-model.ts index 789a909a54..992bc62b1d 100644 --- a/packages/ai/src/mcp/adapters/language-model.ts +++ b/packages/ai/src/mcp/adapters/language-model.ts @@ -40,8 +40,13 @@ export function createMCPLanguageModel(options: MCPLanguageModelOptions): Langua const manager = capabilityManager || new MCPCapabilityManager(client); return { - id: `mcp-${model || 'default'}`, + specificationVersion: 'v1', provider: 'mcp', + modelId: `mcp-${model || 'default'}`, + defaultObjectGenerationMode: undefined, + doStream: async (options: LanguageModelV1CallOptions) => { + throw new Error('Streaming is not supported by the MCP adapter yet'); + }, doGenerate: async (options: LanguageModelV1CallOptions) => { try { // Refresh capabilities if needed @@ -85,6 +90,15 @@ export function createMCPLanguageModel(options: MCPLanguageModelOptions): Langua // Extract the text from the result const text = result.content.map((item) => item.text).join(''); + // Prepare the request body for rawCall + const requestBody = { + messages, + maxTokens, + temperature, + topP, + stopSequences, + }; + return { text, toolCalls: undefined, @@ -92,7 +106,23 @@ export function createMCPLanguageModel(options: MCPLanguageModelOptions): Langua usage: { promptTokens: 0, completionTokens: 0, - totalTokens: 0, + }, + rawCall: { + rawPrompt: messages, + rawSettings: { + maxTokens, + temperature, + topP, + stopSequences, + }, + }, + request: { + body: JSON.stringify(requestBody), + }, + response: { + id: `mcp-response-${Date.now()}`, + timestamp: new Date(), + modelId: `mcp-${model || 'default'}`, }, }; } catch (error) {