Skip to content

Commit 43e1d83

Browse files
committed
Adding MCP tools
1 parent 06abe80 commit 43e1d83

File tree

7 files changed

+400
-0
lines changed

7 files changed

+400
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# MCP proxy for OpenCtx
2+
3+
This is a context provider for [OpenCtx](https://openctx.org) that fetches contents from a [MCP](https://modelcontextprotocol.io) provider for use as context.
4+
5+
Currently, only MCP over stdio is supported (HTTP is not yet supported).
6+
7+
## Development
8+
9+
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`.
10+
1. Run `pnpm watch` in this directory.
11+
1. Add the following to your VS Code settings:
12+
```json
13+
"openctx.providers": {
14+
// ...other providers...
15+
"https://openctx.org/npm/@openctx/provider-modelcontextprotocol": {
16+
"nodeCommand": "node",
17+
"mcp.provider.uri": "file:///path/to/servers/root/build/everything/index.js",
18+
}
19+
}
20+
```
21+
1. Reload the VS Code window. You should see `servers/everything` in the `@`-mention dropdown.
22+
23+
To hook up to the Postgres MCP provider, use:
24+
25+
```json
26+
"openctx.providers": {
27+
// ...other providers...
28+
"https://openctx.org/npm/@openctx/provider-modelcontextprotocol": {
29+
"nodeCommand": "node",
30+
"mcp.provider.uri": "file:///path/to/servers/root/build/postgres/index.js",
31+
"mcp.provider.args": [
32+
"postgresql://sourcegraph:sourcegraph@localhost:5432/sourcegraph"
33+
]
34+
}
35+
}
36+
```
37+
38+
## More MCP Servers
39+
40+
The following MCP servers are available in the [modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers) repository:
41+
42+
- [Brave Search](https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search) - Search the Brave search API
43+
- [Postgres](https://github.com/modelcontextprotocol/servers/tree/main/src/postgres) - Connect to your Postgres databases to query schema information and write optimized SQL
44+
- [Filesystem](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) - Access files on your local machine
45+
- [Everything](https://github.com/modelcontextprotocol/servers/tree/main/src/everything) - A demo server showing MCP capabilities
46+
- [Google Drive](https://github.com/modelcontextprotocol/servers/tree/main/src/gdrive) - Search and access your Google Drive documents
47+
- [Google Maps](https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps) - Get directions and information about places
48+
- [Memo](https://github.com/modelcontextprotocol/servers/tree/main/src/memo) - Access your Memo notes
49+
- [Git](https://github.com/modelcontextprotocol/servers/tree/main/src/git) - Get git history and commit information
50+
- [Puppeteer](https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer) - Control headless Chrome for web automation
51+
- [SQLite](https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite) - Query SQLite databases
52+
53+
## Creating your own MCP server
54+
55+
See the [MCP docs](https://modelcontextprotocol.io) for how to create your own MCP servers.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { describe, expect, test } from 'vitest'
2+
import type { MetaParams, ProviderSettings } from '@openctx/provider'
3+
import proxy from './index.js'
4+
5+
// vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
6+
// Client: vi.fn().mockImplementation(() => ({
7+
// connect: vi.fn(),
8+
// getServerVersion: vi.fn().mockReturnValue({ name: 'Test MCP Server' }),
9+
// request: vi.fn().mockImplementation(async (req) => {
10+
// if (req.method === 'resources/list') {
11+
// return { resources: [
12+
// { uri: 'test://resource', name: 'Test Resource', description: 'Test Description' }
13+
// ]}
14+
// }
15+
// if (req.method === 'resources/read') {
16+
// return { contents: [
17+
// { uri: 'test://resource', text: 'Test Content', mimeType: 'text/plain' }
18+
// ]}
19+
// }
20+
// }),
21+
// setNotificationHandler: vi.fn(),
22+
// setRequestHandler: vi.fn(),
23+
// close: vi.fn()
24+
// }))
25+
// }))
26+
27+
describe('MCP Provider', () => {
28+
const settings: ProviderSettings = {
29+
'mcp.provider.uri': 'file:///Users/arafatkhan/Desktop/servers/src/everything/dist/index.js',
30+
'nodeCommand': 'node',
31+
'mcp.provider.args': []
32+
}
33+
34+
35+
test('meta returns provider info', async () => {
36+
const result = await proxy.meta({} as MetaParams, settings)
37+
38+
39+
// console.log('result', result)
40+
expect(result).toMatchObject({
41+
name: expect.any(String),
42+
mentions: {
43+
label: expect.any(String)
44+
}
45+
})
46+
})
47+
48+
49+
test('MCP Provider > mentions returns resources', async () => {
50+
if (proxy.mentions) {
51+
const result = await proxy.mentions({ query: '' }, {} as ProviderSettings)
52+
53+
// console.log('result', result)
54+
55+
expect(result).toEqual(
56+
expect.arrayContaining([
57+
expect.any(Object)
58+
])
59+
)
60+
} else {
61+
throw new Error('mentions method is not defined on proxy')
62+
}
63+
})
64+
65+
test('MCP Provider > mentions filters resources', async () => {
66+
if (proxy.mentions) {
67+
const result = await proxy.mentions({ query: 'rce 1' }, {} as ProviderSettings)
68+
69+
// console.log('result', result)
70+
71+
expect(result).toEqual(
72+
expect.arrayContaining([
73+
expect.any(Object)
74+
])
75+
)
76+
} else {
77+
throw new Error('mentions method is not defined on proxy')
78+
}
79+
})
80+
81+
test('MCP Provider > mentions filters runny', async () => {
82+
if (proxy.items) {
83+
const result = await proxy.items({ mention: { uri: 'test://static/resource/1', title: 'Resource 1' } }, {} as ProviderSettings)
84+
85+
console.log('result', result)
86+
87+
expect(result).toEqual(
88+
expect.arrayContaining([
89+
expect.any(Object)
90+
])
91+
)
92+
} else {
93+
throw new Error('mentions method is not defined on proxy')
94+
}
95+
})
96+
97+
// test('mentions returns resources', async () => {
98+
// const result = await proxy.mentions?.({
99+
// query: 'test'
100+
// }, settings)
101+
// console.log('final result', result)
102+
// expect(result).toBeDefined()
103+
// expect(Array.isArray(result)).toBe(true)
104+
// })
105+
106+
// test('items returns content', async () => {
107+
// const result = await proxy.items?.({
108+
// mention: {
109+
// uri: 'test://resource',
110+
// title: 'Test Resource'
111+
// }
112+
// }, settings)
113+
114+
// expect(result).toBeDefined()
115+
// expect(Array.isArray(result)).toBe(true)
116+
// })
117+
})
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { basename } from 'node:path'
2+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
3+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
4+
import {
5+
CreateMessageRequestSchema,
6+
ListResourcesResultSchema,
7+
ProgressNotificationSchema,
8+
ReadResourceResultSchema,
9+
} from '@modelcontextprotocol/sdk/types.js'
10+
import type {
11+
Item,
12+
ItemsParams,
13+
ItemsResult,
14+
Mention,
15+
MentionsParams,
16+
MentionsResult,
17+
MetaParams,
18+
MetaResult,
19+
Provider,
20+
ProviderSettings,
21+
} from '@openctx/provider'
22+
23+
async function createClient(
24+
nodeCommand: string,
25+
mcpProviderFile: string,
26+
mcpProviderArgs: string[],
27+
): Promise<Client> {
28+
const client = new Client(
29+
{
30+
name: 'mcp-inspector',
31+
version: '0.0.1',
32+
},
33+
{
34+
capabilities: {
35+
experimental: {},
36+
sampling: {},
37+
roots: {},
38+
},
39+
},
40+
)
41+
const transport = new StdioClientTransport({
42+
command: nodeCommand,
43+
args: [mcpProviderFile, ...mcpProviderArgs],
44+
})
45+
await client.connect(transport)
46+
console.log('connected to MCP server')
47+
48+
client.setNotificationHandler(ProgressNotificationSchema, notification => {
49+
console.log('got MCP notif', notification)
50+
})
51+
52+
client.setRequestHandler(CreateMessageRequestSchema, request => {
53+
console.log('got MCP request', request)
54+
return { _meta: {} }
55+
})
56+
return client
57+
}
58+
59+
class MCPProxy implements Provider {
60+
private mcpClient?: Promise<Client>
61+
62+
async meta(_params: MetaParams, settings: ProviderSettings): Promise<MetaResult> {
63+
const nodeCommand: string = (settings.nodeCommand as string) ?? 'node'
64+
const mcpProviderUri = settings['mcp.provider.uri'] as string
65+
if (!mcpProviderUri) {
66+
this.mcpClient = undefined
67+
return {
68+
name: 'undefined MCP provider',
69+
}
70+
}
71+
if (!mcpProviderUri.startsWith('file://')) {
72+
throw new Error('mcp.provider.uri must be a file:// URI')
73+
}
74+
const mcpProviderFile = mcpProviderUri.slice('file://'.length)
75+
const mcpProviderArgsRaw = settings['mcp.provider.args']
76+
const mcpProviderArgs = Array.isArray(mcpProviderArgsRaw)
77+
? mcpProviderArgsRaw.map(e => `${e}`)
78+
: []
79+
this.mcpClient = createClient(nodeCommand, mcpProviderFile, mcpProviderArgs)
80+
const mcpClient = await this.mcpClient
81+
const serverInfo = mcpClient.getServerVersion()
82+
const name = serverInfo?.name ?? basename(mcpProviderFile)
83+
return {
84+
name,
85+
mentions: {
86+
label: name,
87+
},
88+
}
89+
}
90+
91+
async mentions?(params: MentionsParams, _settings: ProviderSettings): Promise<MentionsResult> {
92+
if (!this.mcpClient) {
93+
return []
94+
}
95+
const mcpClient = await this.mcpClient
96+
const resourcesResp = await mcpClient.request(
97+
{
98+
method: 'resources/list',
99+
params: {},
100+
},
101+
ListResourcesResultSchema,
102+
)
103+
104+
const { resources } = resourcesResp
105+
const mentions: Mention[] = []
106+
for (const resource of resources) {
107+
const r = {
108+
uri: resource.uri,
109+
title: resource.name,
110+
description: resource.description,
111+
}
112+
mentions.push(r)
113+
}
114+
115+
const query = params.query?.trim().toLowerCase()
116+
if (!query) {
117+
return mentions
118+
}
119+
const prefixMatches: Mention[] = []
120+
const substringMatches: Mention[] = []
121+
122+
for (const mention of mentions) {
123+
const title = mention.title.toLowerCase()
124+
if (title.startsWith(query)) {
125+
prefixMatches.push(mention)
126+
} else if (title.includes(query)) {
127+
substringMatches.push(mention)
128+
}
129+
}
130+
131+
return [...prefixMatches, ...substringMatches]
132+
}
133+
134+
async items?(params: ItemsParams, _settings: ProviderSettings): Promise<ItemsResult> {
135+
console.log('items', params)
136+
if (!params.mention || !this.mcpClient) {
137+
return []
138+
}
139+
const mcpClient = await this.mcpClient
140+
const response = await mcpClient.request(
141+
{
142+
method: 'resources/read' as const,
143+
params: { uri: params.mention.uri },
144+
},
145+
ReadResourceResultSchema,
146+
)
147+
148+
const { contents } = response
149+
150+
const items: Item[] = []
151+
for (const content of contents) {
152+
if (content.text) {
153+
items.push({
154+
title: content.uri,
155+
ai: {
156+
content: (content.text as string) ?? '',
157+
},
158+
})
159+
} else {
160+
console.log('No text field was present, mimeType was', content.mimeType)
161+
}
162+
}
163+
return items
164+
}
165+
166+
dispose?(): void {
167+
if (this.mcpClient) {
168+
this.mcpClient.then(c => {
169+
c.close()
170+
})
171+
}
172+
}
173+
}
174+
175+
const proxy = new MCPProxy()
176+
export default proxy
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@openctx/provider-modelcontextprotocol",
3+
"version": "0.0.13",
4+
"description": "Use information from MCP providers",
5+
"license": "Apache-2.0",
6+
"homepage": "https://openctx.org/docs/providers/modelcontextprotocol",
7+
"repository": {
8+
"type": "git",
9+
"url": "https://github.com/sourcegraph/openctx",
10+
"directory": "provider/modelcontextprotocol"
11+
},
12+
"type": "module",
13+
"main": "dist/bundle.js",
14+
"types": "dist/index.d.ts",
15+
"files": [
16+
"dist/bundle.js",
17+
"dist/index.d.ts"
18+
],
19+
"sideEffects": false,
20+
"scripts": {
21+
"bundle": "tsc --build && esbuild --log-level=error --platform=node --bundle --format=esm --outfile=dist/bundle.js index.ts",
22+
"prepublishOnly": "tsc --build --clean && npm run --silent bundle",
23+
"test": "vitest",
24+
"test:unit": "vitest run",
25+
"watch": "tsc --build --watch & esbuild --log-level=error --platform=node --bundle --format=esm --outfile=dist/bundle.js --watch index.ts"
26+
},
27+
"dependencies": {
28+
"@openctx/provider": "workspace:*",
29+
"@modelcontextprotocol/sdk": "1.0.1",
30+
"express": "^4.21.1",
31+
"zod": "^3.23.8",
32+
"zod-to-json-schema": "^3.23.5"
33+
34+
}
35+
}

0 commit comments

Comments
 (0)