From 504b76c233792413fa6fc03c1260063418656de2 Mon Sep 17 00:00:00 2001 From: arafatkatze Date: Sat, 11 Jan 2025 21:07:56 +0400 Subject: [PATCH] Adding mcptools --- package.json | 2 + provider/modelcontextprotocoltools/.gitignore | 5 + provider/modelcontextprotocoltools/README.md | 55 +++++ provider/modelcontextprotocoltools/index.ts | 194 ++++++++++++++++++ .../modelcontextprotocoltools/package.json | 44 ++++ .../modelcontextprotocoltools/tsconfig.json | 11 + 6 files changed, 311 insertions(+) create mode 100644 provider/modelcontextprotocoltools/.gitignore create mode 100644 provider/modelcontextprotocoltools/README.md create mode 100644 provider/modelcontextprotocoltools/index.ts create mode 100644 provider/modelcontextprotocoltools/package.json create mode 100644 provider/modelcontextprotocoltools/tsconfig.json diff --git a/package.json b/package.json index 008f3d46..b72d0895 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,8 @@ "ts-node": "^10.9.2", "typescript": "^5.4.5", "vite": "^5.2.11", + "json-schema-to-zod": "^0.1.5", + "@apidevtools/json-schema-ref-parser": "^11.7.3", "vitest": "^1.6.0" }, "stylelint": { diff --git a/provider/modelcontextprotocoltools/.gitignore b/provider/modelcontextprotocoltools/.gitignore new file mode 100644 index 00000000..3d4b0ca5 --- /dev/null +++ b/provider/modelcontextprotocoltools/.gitignore @@ -0,0 +1,5 @@ +index.test.ts +package-lock.json +dist/ +vitest.config.ts +vitest.config.t \ No newline at end of file diff --git a/provider/modelcontextprotocoltools/README.md b/provider/modelcontextprotocoltools/README.md new file mode 100644 index 00000000..c132c1f4 --- /dev/null +++ b/provider/modelcontextprotocoltools/README.md @@ -0,0 +1,55 @@ +# MCP proxy for OpenCtx + +This is a context provider for [OpenCtx](https://openctx.org) that fetches contents from a [MCP](https://modelcontextprotocol.io) provider for use as context. + +Currently, only MCP over stdio is supported (HTTP is not yet supported). + +## Development + +1. Clone the [modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers) repository. Follow the instructions there to build the example providers. This should generate output files of the form `build/${example_name}/index.js`. +1. Run `pnpm watch` in this directory. +1. Add the following to your VS Code settings: + ```json + "openctx.providers": { + // ...other providers... + "https://openctx.org/npm/@openctx/provider-modelcontextprotocol": { + "nodeCommand": "node", + "mcp.provider.uri": "file:///path/to/servers/root/build/everything/index.js", + } + } + ``` +1. Reload the VS Code window. You should see `servers/everything` in the `@`-mention dropdown. + +To hook up to the Postgres MCP provider, use: + +```json +"openctx.providers": { + // ...other providers... + "https://openctx.org/npm/@openctx/provider-modelcontextprotocol": { + "nodeCommand": "node", + "mcp.provider.uri": "file:///path/to/servers/root/build/postgres/index.js", + "mcp.provider.args": [ + "postgresql://sourcegraph:sourcegraph@localhost:5432/sourcegraph" + ] + } +} +``` + +## More MCP Servers + +The following MCP servers are available in the [modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers) repository: + +- [Brave Search](https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search) - Search the Brave search API +- [Postgres](https://github.com/modelcontextprotocol/servers/tree/main/src/postgres) - Connect to your Postgres databases to query schema information and write optimized SQL +- [Filesystem](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) - Access files on your local machine +- [Everything](https://github.com/modelcontextprotocol/servers/tree/main/src/everything) - A demo server showing MCP capabilities +- [Google Drive](https://github.com/modelcontextprotocol/servers/tree/main/src/gdrive) - Search and access your Google Drive documents +- [Google Maps](https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps) - Get directions and information about places +- [Memo](https://github.com/modelcontextprotocol/servers/tree/main/src/memo) - Access your Memo notes +- [Git](https://github.com/modelcontextprotocol/servers/tree/main/src/git) - Get git history and commit information +- [Puppeteer](https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer) - Control headless Chrome for web automation +- [SQLite](https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite) - Query SQLite databases + +## Creating your own MCP server + +See the [MCP docs](https://modelcontextprotocol.io) for how to create your own MCP servers. \ No newline at end of file diff --git a/provider/modelcontextprotocoltools/index.ts b/provider/modelcontextprotocoltools/index.ts new file mode 100644 index 00000000..7c41db08 --- /dev/null +++ b/provider/modelcontextprotocoltools/index.ts @@ -0,0 +1,194 @@ +import { basename } from 'node:path' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' +import { + CallToolResultSchema, + +} from '@modelcontextprotocol/sdk/types.js' +import type { + Item, + ItemsParams, + ItemsResult, + Mention, + MentionsParams, + MentionsResult, + MetaParams, + MetaResult, + Provider, + ProviderSettings, +} from '@openctx/provider' +const Ajv = require('ajv') + +async function createClient( + nodeCommand: string, + mcpProviderFile: string, + mcpProviderArgs: string[], +): Promise { + const client = new Client( + { + name: 'mcp-tool', + version: '0.0.1', + }, + { + capabilities: { + experimental: {}, + sampling: {}, + roots: {}, + }, + }, + ) + const transport = new StdioClientTransport({ + command: nodeCommand, + args: [mcpProviderFile, ...mcpProviderArgs], + }) + await client.connect(transport) + return client +} + +class MCPToolsProxy implements Provider { + private mcpClient?: Promise + private toolSchemas: Map = new Map() + private ajv = new Ajv() + + // Gets the Metadata for the MCP Tools Provider + async meta(_params: MetaParams, settings: ProviderSettings): Promise { + const nodeCommand: string = (settings.nodeCommand as string) ?? 'node' + const mcpProviderUri = settings['mcp.provider.uri'] as string + if (!mcpProviderUri) { + this.mcpClient = undefined + return { + name: 'undefined MCP provider', + } + } + if (!mcpProviderUri.startsWith('file://')) { + throw new Error('mcp.provider.uri must be a file:// URI') + } + const mcpProviderFile = mcpProviderUri.slice('file://'.length) + const mcpProviderArgsRaw = settings['mcp.provider.args'] + const mcpProviderArgs = Array.isArray(mcpProviderArgsRaw) + ? mcpProviderArgsRaw.map(e => `${e}`) + : [] + this.mcpClient = createClient(nodeCommand, mcpProviderFile, mcpProviderArgs) + const mcpClient = await this.mcpClient + const serverInfo = mcpClient.getServerVersion() + const name = serverInfo?.name ?? basename(mcpProviderFile) + return { + name, + mentions: { + label: name, + }, + } + } + + // Gets Lists All the tools available in the MCP Provider along with their schemas + async mentions?(params: MentionsParams, _settings: ProviderSettings): Promise { + if (!this.mcpClient) { + return [] + } + const mcpClient = await this.mcpClient + const toolsResp = await mcpClient.listTools() + + const { tools } = toolsResp + const mentions: Mention[] = [] + for (const tool of tools) { + // Store the schema in the Map using tool name as key + this.toolSchemas.set(tool.name, JSON.stringify(tool.inputSchema)) + + const r = { + uri: tool.uri, + title: tool.name, + description: tool.description, + data: (tool.inputSchema), + } as Mention + mentions.push(r) + } + + const query = params.query?.trim().toLowerCase() + if (!query) { + return mentions + } + const prefixMatches: Mention[] = [] + const substringMatches: Mention[] = [] + + // Filters the tools based on the query + for (const mention of mentions) { + const title = mention.title.toLowerCase() + if (title.startsWith(query)) { + prefixMatches.push(mention) + } else if (title.includes(query)) { + substringMatches.push(mention) + } + } + + // Combines the prefix and substring matches + return [...prefixMatches, ...substringMatches] + } + + // Retrieves the schema for a tool from the Map using the tool name as key + getToolSchema(toolName: string): any { + return JSON.parse(this.toolSchemas.get(toolName) as string) + } + + // Calls the tool with the provided input and returns the result + async items?(params: ItemsParams, _settings: ProviderSettings): Promise { + if (!this.mcpClient) { + return [] + } + const mcpClient = await this.mcpClient + + const toolName = params.mention?.title + const toolInput = params.mention?.data + + // Validates the tool input against the stored schema + if (toolName && toolInput) { + const schema = this.getToolSchema(toolName) + if (schema) { + const isValid = this.ajv.validate(schema, toolInput) + if (!isValid) { + console.error('Invalid tool input:', this.ajv.errors) + throw new Error(`Invalid input for tool ${toolName}: ${JSON.stringify(this.ajv.errors)}`) + } + } + } + + // Calls the tool with the provided input + const response = await mcpClient.request( + { + method: 'tools/call' as const, + params: { + name: toolName, + arguments: toolInput + }, + }, + CallToolResultSchema, + ) + + const contents = response.content + const items: Item[] = [] + for (const content of contents) { + if (content.text) { + items.push({ + title: (toolName as string) ?? '', + ai: { + content: (content.text as string) ?? '', + }, + }) + } else { + console.log('No text field was present, mimeType was', content.mimeType) + } + } + return items + } + + dispose?(): void { + if (this.mcpClient) { + this.mcpClient.then(c => { + c.close() + }) + } + } +} + + +const proxy = new MCPToolsProxy() +export default proxy diff --git a/provider/modelcontextprotocoltools/package.json b/provider/modelcontextprotocoltools/package.json new file mode 100644 index 00000000..839bf88e --- /dev/null +++ b/provider/modelcontextprotocoltools/package.json @@ -0,0 +1,44 @@ +{ + "name": "@openctx/provider-modelcontextprotocoltools", + "version": "0.0.13", + "description": "Use information from MCP providers", + "license": "Apache-2.0", + "homepage": "https://openctx.org/docs/providers/modelcontextprotocoltools", + "repository": { + "type": "git", + "url": "https://github.com/sourcegraph/openctx", + "directory": "provider/modelcontextprotocoltools" + }, + "type": "module", + "main": "dist/bundle.js", + "types": "dist/index.d.ts", + "files": [ + "dist/bundle.js", + "dist/index.d.ts" + ], + "sideEffects": false, + "scripts": { + "bundle": "tsc --build && esbuild --log-level=error --platform=node --bundle --format=esm --outfile=dist/bundle.js index.ts", + "prepublishOnly": "tsc --build --clean && npm run --silent bundle", + "test": "vitest", + "test:unit": "vitest run", + "watch": "tsc --build --watch & esbuild --log-level=error --platform=node --bundle --format=esm --outfile=dist/bundle.js --watch index.ts" + }, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.7.3", + "@modelcontextprotocol/sdk": "1.0.1", + "@openctx/provider": "workspace:*", + "ajv": "^8.17.1", + "express": "^4.21.1", + "json-schema-to-zod": "^0.1.5", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.24.1" + }, + "pnpm": { + "peerDependencyRules": { + "allowAny": [ + "@apidevtools/json-schema-ref-parser" + ] + } + } +} diff --git a/provider/modelcontextprotocoltools/tsconfig.json b/provider/modelcontextprotocoltools/tsconfig.json new file mode 100644 index 00000000..a1d94187 --- /dev/null +++ b/provider/modelcontextprotocoltools/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../.config/tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "dist", + "lib": ["ESNext"] + }, + "include": ["*.ts"], + "exclude": ["dist", "vitest.config.ts"], + "references": [{ "path": "../../lib/provider" }] +}