-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
06abe80
commit 43e1d83
Showing
7 changed files
with
400 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { describe, expect, test } from 'vitest' | ||
import type { MetaParams, ProviderSettings } from '@openctx/provider' | ||
import proxy from './index.js' | ||
|
||
// vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({ | ||
// Client: vi.fn().mockImplementation(() => ({ | ||
// connect: vi.fn(), | ||
// getServerVersion: vi.fn().mockReturnValue({ name: 'Test MCP Server' }), | ||
// request: vi.fn().mockImplementation(async (req) => { | ||
// if (req.method === 'resources/list') { | ||
// return { resources: [ | ||
// { uri: 'test://resource', name: 'Test Resource', description: 'Test Description' } | ||
// ]} | ||
// } | ||
// if (req.method === 'resources/read') { | ||
// return { contents: [ | ||
// { uri: 'test://resource', text: 'Test Content', mimeType: 'text/plain' } | ||
// ]} | ||
// } | ||
// }), | ||
// setNotificationHandler: vi.fn(), | ||
// setRequestHandler: vi.fn(), | ||
// close: vi.fn() | ||
// })) | ||
// })) | ||
|
||
describe('MCP Provider', () => { | ||
const settings: ProviderSettings = { | ||
'mcp.provider.uri': 'file:///Users/arafatkhan/Desktop/servers/src/everything/dist/index.js', | ||
'nodeCommand': 'node', | ||
'mcp.provider.args': [] | ||
} | ||
|
||
|
||
test('meta returns provider info', async () => { | ||
const result = await proxy.meta({} as MetaParams, settings) | ||
|
||
|
||
// console.log('result', result) | ||
expect(result).toMatchObject({ | ||
name: expect.any(String), | ||
mentions: { | ||
label: expect.any(String) | ||
} | ||
}) | ||
}) | ||
|
||
|
||
test('MCP Provider > mentions returns resources', async () => { | ||
if (proxy.mentions) { | ||
const result = await proxy.mentions({ query: '' }, {} as ProviderSettings) | ||
|
||
// console.log('result', result) | ||
|
||
expect(result).toEqual( | ||
expect.arrayContaining([ | ||
expect.any(Object) | ||
]) | ||
) | ||
} else { | ||
throw new Error('mentions method is not defined on proxy') | ||
} | ||
}) | ||
|
||
test('MCP Provider > mentions filters resources', async () => { | ||
if (proxy.mentions) { | ||
const result = await proxy.mentions({ query: 'rce 1' }, {} as ProviderSettings) | ||
|
||
// console.log('result', result) | ||
|
||
expect(result).toEqual( | ||
expect.arrayContaining([ | ||
expect.any(Object) | ||
]) | ||
) | ||
} else { | ||
throw new Error('mentions method is not defined on proxy') | ||
} | ||
}) | ||
|
||
test('MCP Provider > mentions filters runny', async () => { | ||
if (proxy.items) { | ||
const result = await proxy.items({ mention: { uri: 'test://static/resource/1', title: 'Resource 1' } }, {} as ProviderSettings) | ||
|
||
console.log('result', result) | ||
|
||
expect(result).toEqual( | ||
expect.arrayContaining([ | ||
expect.any(Object) | ||
]) | ||
) | ||
} else { | ||
throw new Error('mentions method is not defined on proxy') | ||
} | ||
}) | ||
|
||
// test('mentions returns resources', async () => { | ||
// const result = await proxy.mentions?.({ | ||
// query: 'test' | ||
// }, settings) | ||
// console.log('final result', result) | ||
// expect(result).toBeDefined() | ||
// expect(Array.isArray(result)).toBe(true) | ||
// }) | ||
|
||
// test('items returns content', async () => { | ||
// const result = await proxy.items?.({ | ||
// mention: { | ||
// uri: 'test://resource', | ||
// title: 'Test Resource' | ||
// } | ||
// }, settings) | ||
|
||
// expect(result).toBeDefined() | ||
// expect(Array.isArray(result)).toBe(true) | ||
// }) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
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<Client> { | ||
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<Client> | ||
|
||
async meta(_params: MetaParams, settings: ProviderSettings): Promise<MetaResult> { | ||
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<MentionsResult> { | ||
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<ItemsResult> { | ||
console.log('items', params) | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
{ | ||
"name": "@openctx/provider-modelcontextprotocol", | ||
"version": "0.0.13", | ||
"description": "Use information from MCP providers", | ||
"license": "Apache-2.0", | ||
"homepage": "https://openctx.org/docs/providers/modelcontextprotocol", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/sourcegraph/openctx", | ||
"directory": "provider/modelcontextprotocol" | ||
}, | ||
"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": { | ||
"@openctx/provider": "workspace:*", | ||
"@modelcontextprotocol/sdk": "1.0.1", | ||
"express": "^4.21.1", | ||
"zod": "^3.23.8", | ||
"zod-to-json-schema": "^3.23.5" | ||
|
||
} | ||
} |
Oops, something went wrong.