From 68b0ef0242870ddabeddaed453a31e2343307f45 Mon Sep 17 00:00:00 2001 From: Chris Sev Date: Mon, 25 Nov 2024 08:59:23 -0700 Subject: [PATCH 1/2] mcp --- provider/modelcontextprotocol/README.md | 54 ++++++ provider/modelcontextprotocol/index.ts | 175 ++++++++++++++++++ provider/modelcontextprotocol/package.json | 30 +++ provider/modelcontextprotocol/tsconfig.json | 11 ++ .../modelcontextprotocol/vitest.config.ts | 3 + .../docs/providers/modelcontextprotocol.mdx | 8 + 6 files changed, 281 insertions(+) create mode 100644 provider/modelcontextprotocol/README.md create mode 100644 provider/modelcontextprotocol/index.ts create mode 100644 provider/modelcontextprotocol/package.json create mode 100644 provider/modelcontextprotocol/tsconfig.json create mode 100644 provider/modelcontextprotocol/vitest.config.ts create mode 100644 web/content/docs/providers/modelcontextprotocol.mdx diff --git a/provider/modelcontextprotocol/README.md b/provider/modelcontextprotocol/README.md new file mode 100644 index 00000000..54b8219c --- /dev/null +++ b/provider/modelcontextprotocol/README.md @@ -0,0 +1,54 @@ +# MCP proxy for OpenCtx + +This is a context provider for [OpenCtx](https://openctx.org) that fetches contents from a [MCP](https://github.com/modelcontextprotocol/specification) 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/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/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: + +- [Postgres](https://github.com/modelcontextprotocol/servers/tree/main/src/postgres) - Connect to your Postgres databases to query schema information and write optimized SQL +- [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 +- [Giphy](https://github.com/modelcontextprotocol/servers/tree/main/src/giphy) - Search gifs +- [Git](https://github.com/modelcontextprotocol/servers/tree/main/src/git) - Get git history and commit information +- [HubSpot](https://github.com/modelcontextprotocol/servers/tree/main/src/hubspot) - Access your HubSpot CRM data +- [OSAScript](https://github.com/modelcontextprotocol/servers/tree/main/src/osascript) - Execute AppleScript commands on macOS +- [Puppeteer](https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer) - Control headless Chrome for web automation +- [Spotify](https://github.com/modelcontextprotocol/servers/tree/main/src/spotify) - Access Spotify music data and playlists + +## 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/modelcontextprotocol/index.ts b/provider/modelcontextprotocol/index.ts new file mode 100644 index 00000000..14817a30 --- /dev/null +++ b/provider/modelcontextprotocol/index.ts @@ -0,0 +1,175 @@ +import { basename } from 'node:path' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' +import { + CreateMessageRequestSchema, + ListResourcesResultSchema, + ProgressNotificationSchema, + ReadResourceResultSchema, +} from '@modelcontextprotocol/sdk/types.js' +import type { + Item, + ItemsParams, + ItemsResult, + Mention, + MentionsParams, + MentionsResult, + MetaParams, + MetaResult, + Provider, + ProviderSettings, +} from '@openctx/provider' + +async function createClient( + nodeCommand: string, + mcpProviderFile: string, + mcpProviderArgs: string[], +): Promise { + const client = new Client( + { + name: 'mcp-inspector', + version: '0.0.1', + }, + { + capabilities: { + experimental: {}, + sampling: {}, + roots: {}, + }, + }, + ) + const transport = new StdioClientTransport({ + command: nodeCommand, + args: [mcpProviderFile, ...mcpProviderArgs], + }) + await client.connect(transport) + console.log('connected to MCP server') + + client.setNotificationHandler(ProgressNotificationSchema, notification => { + console.log('got MCP notif', notification) + }) + + client.setRequestHandler(CreateMessageRequestSchema, request => { + console.log('got MCP request', request) + return { _meta: {} } + }) + return client +} + +class MCPProxy implements Provider { + private mcpClient?: Promise + + 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, + }, + } + } + + async mentions?(params: MentionsParams, _settings: ProviderSettings): Promise { + if (!this.mcpClient) { + return [] + } + const mcpClient = await this.mcpClient + const resourcesResp = await mcpClient.request( + { + method: 'resources/list', + params: {}, + }, + ListResourcesResultSchema, + ) + + const { resources } = resourcesResp + const mentions: Mention[] = [] + for (const resource of resources) { + const r = { + uri: resource.uri, + title: resource.name, + description: resource.description, + } + mentions.push(r) + } + + const query = params.query?.trim().toLowerCase() + if (!query) { + return mentions + } + const prefixMatches: Mention[] = [] + const substringMatches: Mention[] = [] + + 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) + } + } + + return [...prefixMatches, ...substringMatches] + } + + async items?(params: ItemsParams, _settings: ProviderSettings): Promise { + if (!params.mention || !this.mcpClient) { + return [] + } + const mcpClient = await this.mcpClient + const response = await mcpClient.request( + { + method: 'resources/read' as const, + params: { uri: params.mention.uri }, + }, + ReadResourceResultSchema, + ) + + const { contents } = response + + const items: Item[] = [] + for (const content of contents) { + if (content.text) { + items.push({ + title: content.uri, + 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 MCPProxy() +export default proxy diff --git a/provider/modelcontextprotocol/package.json b/provider/modelcontextprotocol/package.json new file mode 100644 index 00000000..4ab542bb --- /dev/null +++ b/provider/modelcontextprotocol/package.json @@ -0,0 +1,30 @@ +{ + "name": "@openctx/provider-mcp", + "version": "0.0.11", + "description": "Use information from MCP providers", + "license": "Apache-2.0", + "homepage": "https://openctx.org/docs/providers/mcp", + "repository": { + "type": "git", + "url": "https://github.com/sourcegraph/openctx", + "directory": "provider/mcp" + }, + "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", + "watch": "tsc --build --watch & esbuild --log-level=error --platform=node --bundle --format=esm --outfile=dist/bundle.js --watch index.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^0.7.0", + "@openctx/provider": "workspace:*" + } +} diff --git a/provider/modelcontextprotocol/tsconfig.json b/provider/modelcontextprotocol/tsconfig.json new file mode 100644 index 00000000..a1d94187 --- /dev/null +++ b/provider/modelcontextprotocol/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" }] +} diff --git a/provider/modelcontextprotocol/vitest.config.ts b/provider/modelcontextprotocol/vitest.config.ts new file mode 100644 index 00000000..abed6b21 --- /dev/null +++ b/provider/modelcontextprotocol/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({}) diff --git a/web/content/docs/providers/modelcontextprotocol.mdx b/web/content/docs/providers/modelcontextprotocol.mdx new file mode 100644 index 00000000..47a1d35b --- /dev/null +++ b/web/content/docs/providers/modelcontextprotocol.mdx @@ -0,0 +1,8 @@ +export const info = { + title: 'Anthropic Model Context Protocol', + group: 'providers', +} + +import Readme from '../../../../provider/modelcontextprotocol/README.md' + + \ No newline at end of file From 939e9764fb31c90d6e86f485dec50108613ede75 Mon Sep 17 00:00:00 2001 From: Chris Sev Date: Mon, 25 Nov 2024 09:01:59 -0700 Subject: [PATCH 2/2] updating pnpm lockfile --- pnpm-lock.yaml | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 522cbb0c..7a874611 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -640,6 +640,15 @@ importers: specifier: workspace:* version: link:../../lib/provider + provider/modelcontextprotocol: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^0.7.0 + version: 0.7.0 + '@openctx/provider': + specifier: workspace:* + version: link:../../lib/provider + provider/notion: dependencies: '@notionhq/client': @@ -4135,6 +4144,14 @@ packages: resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} dev: false + /@modelcontextprotocol/sdk@0.7.0: + resolution: {integrity: sha512-YlnQf8//eDHClUM607vb/6+GHmCdMnIfOkN2pcpexN4go9sYHm2JfNnqc5ILS7M8enUlwe9dQO9886l3NO3rUw==} + dependencies: + content-type: 1.0.5 + raw-body: 3.0.0 + zod: 3.23.8 + dev: false + /@ndelangen/get-tarball@3.0.9: resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==} dependencies: @@ -8233,6 +8250,11 @@ packages: resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} engines: {node: '>= 0.6'} + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + /convert-source-map@1.7.0: resolution: {integrity: sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==} dependencies: @@ -10291,6 +10313,13 @@ packages: dependencies: safer-buffer: 2.1.2 + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} requiresBuild: true @@ -13173,6 +13202,16 @@ packages: iconv-lite: 0.4.24 unpipe: 1.0.0 + /raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + dev: false + /rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -15726,6 +15765,10 @@ packages: engines: {node: '>=12.20'} dev: true + /zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + dev: false + /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: false