Skip to content

Commit

Permalink
Adding MCP tools
Browse files Browse the repository at this point in the history
  • Loading branch information
arafatkatze committed Jan 8, 2025
1 parent 06abe80 commit 43e1d83
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 0 deletions.
55 changes: 55 additions & 0 deletions provider/modelcontextprotocoltools/README.md
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.
117 changes: 117 additions & 0 deletions provider/modelcontextprotocoltools/index.test.ts
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)
// })
})
176 changes: 176 additions & 0 deletions provider/modelcontextprotocoltools/index.ts
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
35 changes: 35 additions & 0 deletions provider/modelcontextprotocoltools/package.json
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"

}
}
Loading

0 comments on commit 43e1d83

Please sign in to comment.