Skip to content

Commit

Permalink
Adding mcptools
Browse files Browse the repository at this point in the history
  • Loading branch information
arafatkatze committed Jan 11, 2025
1 parent d5a4ab6 commit 504b76c
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 0 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
5 changes: 5 additions & 0 deletions provider/modelcontextprotocoltools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
index.test.ts
package-lock.json
dist/
vitest.config.ts
vitest.config.t
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.
194 changes: 194 additions & 0 deletions provider/modelcontextprotocoltools/index.ts
Original file line number Diff line number Diff line change
@@ -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<Client> {
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<Client>
private toolSchemas: Map<string, any> = new Map()
private ajv = new Ajv()

// Gets the Metadata for the MCP Tools Provider
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,
},
}
}

// Gets Lists All the tools available in the MCP Provider along with their schemas
async mentions?(params: MentionsParams, _settings: ProviderSettings): Promise<MentionsResult> {
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<ItemsResult> {
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
44 changes: 44 additions & 0 deletions provider/modelcontextprotocoltools/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
}
11 changes: 11 additions & 0 deletions provider/modelcontextprotocoltools/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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" }]
}

0 comments on commit 504b76c

Please sign in to comment.