feat: add MCP JSON schema and documentation

fixes #462
This commit is contained in:
can1357
2026-03-18 22:26:13 +01:00
parent 38eeeeda1b
commit 66c80fd613
10 changed files with 705 additions and 5 deletions
+2 -1
View File
@@ -238,9 +238,10 @@ Full Model Context Protocol support with external tool integration:
- Stdio and HTTP transports for connecting to MCP servers
- **OAuth support**: Explicit `clientId` and `callbackPort` in MCP server config, manual OAuth callbacks via slash commands
- **Browser server filtering**: Automatically filters browser-type MCP servers to prevent conflicts with built-in browser tool
- **Automatic Exa filtering**: Extracts Exa API keys and prefers the native Exa integration
- **Config schema + setup guide**: [`docs/mcp-config.md`](./docs/mcp-config.md) and [`packages/coding-agent/src/config/mcp-schema.json`](./packages/coding-agent/src/config/mcp-schema.json)
- Plugin CLI (`omp plugin install/enable/configure/doctor`)
- Hot-loadable plugins from `~/.omp/plugins/` with npm/bun integration
- Automatic Exa MCP server filtering with API key extraction
- `disabledServers` works on both project-level and user-level third-party servers
### + Web Search & Fetch
+449
View File
@@ -0,0 +1,449 @@
# MCP configuration in OMP
This guide explains how to add, edit, and validate MCP servers for the OMP coding agent.
Source of truth in code:
- Runtime config types: `packages/coding-agent/src/mcp/types.ts`
- Config writer: `packages/coding-agent/src/mcp/config-writer.ts`
- Loader + validation: `packages/coding-agent/src/mcp/config.ts`
- Standalone `mcp.json` discovery: `packages/coding-agent/src/discovery/mcp-json.ts`
- Schema: `packages/coding-agent/src/config/mcp-schema.json`
## Preferred config locations
OMP can discover MCP servers from multiple tools (`.claude/`, `.cursor/`, `.vscode/`, `opencode.json`, and more), but for OMP-native configuration you should usually use one of these files:
- Project: `.omp/mcp.json`
- User: `~/.omp/mcp.json`
OMP also accepts fallback standalone files in the project root:
- `mcp.json`
- `.mcp.json`
Use `.omp/mcp.json` when you want OMP to own the configuration. Use root `mcp.json` / `.mcp.json` only when you want a portable fallback file that other MCP clients may also read.
## Add a schema reference
Add this line at the top of the file for editor autocomplete and validation:
```json
{
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"mcpServers": {}
}
```
OMP now writes this automatically when `/mcp add`, `/mcp enable`, `/mcp disable`, `/mcp reauth`, or other config-writing flows create or update an OMP-managed MCP file.
## File shape
OMP supports this top-level structure:
```json
{
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"mcpServers": {
"server-name": {
"type": "stdio",
"command": "npx",
"args": ["-y", "some-mcp-server"]
}
},
"disabledServers": ["server-name"]
}
```
Top-level keys:
- `$schema` — optional JSON Schema URL for tooling
- `mcpServers` — map of server name to server config
- `disabledServers` — user-level denylist used to turn off discovered servers by name
Server names must match `^[a-zA-Z0-9_.-]{1,100}$`.
## Supported server fields
Shared fields for every transport:
- `enabled?: boolean` — skip this server when `false`
- `timeout?: number` — connection timeout in milliseconds
- `auth?: { ... }` — auth metadata used by OMP for OAuth/API-key flows
- `oauth?: { ... }` — explicit OAuth client settings used during auth/reauth
### `stdio` transport
`stdio` is the default when `type` is omitted.
Required:
- `command: string`
Optional:
- `type?: "stdio"`
- `args?: string[]`
- `env?: Record<string, string>`
- `cwd?: string`
Example:
```json
{
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/alice/projects",
"/Users/alice/Documents"
]
}
}
}
```
This follows the official Filesystem MCP server package (`@modelcontextprotocol/server-filesystem`).
### `http` transport
Required:
- `type: "http"`
- `url: string`
Optional:
- `headers?: Record<string, string>`
Example:
```json
{
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"mcpServers": {
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/"
}
}
}
```
This matches GitHub's hosted GitHub MCP server endpoint.
### `sse` transport
Required:
- `type: "sse"`
- `url: string`
Optional:
- `headers?: Record<string, string>`
Example:
```json
{
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"mcpServers": {
"legacy-remote": {
"type": "sse",
"url": "https://example.com/mcp/sse"
}
}
}
```
`sse` is still supported for compatibility, but the MCP spec now prefers Streamable HTTP (`type: "http"`) for new servers.
## Auth fields
OMP understands two auth-related objects.
### `auth`
```json
{
"type": "oauth" | "apikey",
"credentialId": "optional-stored-credential-id",
"tokenUrl": "optional-token-endpoint",
"clientId": "optional-client-id",
"clientSecret": "optional-client-secret"
}
```
Use this when OMP should remember how to rehydrate credentials for a server.
### `oauth`
```json
{
"clientId": "...",
"clientSecret": "...",
"redirectUri": "...",
"callbackPort": 3334,
"callbackPath": "/oauth/callback"
}
```
Use this when the MCP server requires explicit OAuth client settings.
Slack is the clearest current example. Slack's MCP server is hosted at `https://mcp.slack.com/mcp`, uses Streamable HTTP, and requires confidential OAuth with your Slack app's client credentials.
Example:
```json
{
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"mcpServers": {
"slack": {
"type": "http",
"url": "https://mcp.slack.com/mcp",
"oauth": {
"clientId": "YOUR_SLACK_CLIENT_ID",
"clientSecret": "YOUR_SLACK_CLIENT_SECRET"
},
"auth": {
"type": "oauth",
"tokenUrl": "https://slack.com/api/oauth.v2.user.access",
"clientId": "YOUR_SLACK_CLIENT_ID",
"clientSecret": "YOUR_SLACK_CLIENT_SECRET"
}
}
}
}
```
Relevant Slack endpoints from Slack's docs:
- MCP endpoint: `https://mcp.slack.com/mcp`
- Authorization endpoint: `https://slack.com/oauth/v2_user/authorize`
- Token endpoint: `https://slack.com/api/oauth.v2.user.access`
## Common copy-paste examples
### Filesystem server via stdio
```json
{
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/absolute/path/one",
"/absolute/path/two"
]
}
}
}
```
### GitHub hosted server via HTTP
```json
{
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"mcpServers": {
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/"
}
}
}
```
### GitHub local server via Docker
```json
{
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"mcpServers": {
"github": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "GITHUB_PERSONAL_ACCESS_TOKEN"
}
}
}
}
```
This matches GitHub's official local Docker image `ghcr.io/github/github-mcp-server`.
### Slack hosted server via OAuth
```json
{
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"mcpServers": {
"slack": {
"type": "http",
"url": "https://mcp.slack.com/mcp",
"oauth": {
"clientId": "YOUR_SLACK_CLIENT_ID",
"clientSecret": "YOUR_SLACK_CLIENT_SECRET"
},
"auth": {
"type": "oauth",
"tokenUrl": "https://slack.com/api/oauth.v2.user.access",
"clientId": "YOUR_SLACK_CLIENT_ID",
"clientSecret": "YOUR_SLACK_CLIENT_SECRET"
}
}
}
}
```
## Secrets and variable resolution
This is the part that usually trips people up.
### In `.omp/mcp.json` and `~/.omp/mcp.json`
Before OMP launches a server or makes an HTTP request, it resolves `env` and `headers` values like this:
1. If a value starts with `!`, OMP runs it as a shell command and uses trimmed stdout.
2. Otherwise OMP first checks whether the value matches an environment variable name.
3. If that environment variable is not set, OMP uses the string literally.
Examples:
```json
{
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "GITHUB_PERSONAL_ACCESS_TOKEN"
},
"headers": {
"X-MCP-Insiders": "true"
}
}
```
That means this is valid and convenient for local secrets:
- `"GITHUB_PERSONAL_ACCESS_TOKEN": "GITHUB_PERSONAL_ACCESS_TOKEN"` → copy from the current shell environment
- `"Authorization": "Bearer hardcoded-token"` → use the literal value
- `"Authorization": "!printf 'Bearer %s' \"$GITHUB_TOKEN\""` → build the header from a command
### In root `mcp.json` and `.mcp.json`
The standalone fallback loader also expands `${VAR}` and `${VAR:-default}` inside strings during discovery.
Example:
```json
{
"mcpServers": {
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": {
"Authorization": "Bearer ${GITHUB_TOKEN}"
}
}
}
}
```
If you want the least surprising OMP behavior, prefer `.omp/mcp.json` and use explicit env/header values.
## `disabledServers`
`disabledServers` is mainly useful in the user config file (`~/.omp/mcp.json`) when a server is discovered from some other source and you want OMP to ignore it without editing that other tool's config.
Example:
```json
{
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"disabledServers": ["github", "slack"]
}
```
## `/mcp add` vs editing JSON directly
Use `/mcp add` when you want guided setup.
Use direct JSON editing when:
- you need a transport or auth option the wizard does not prompt for yet
- you want to paste a server definition from another MCP client
- you want schema-backed validation in your editor
After editing, use:
- `/mcp reload` to rediscover and reconnect servers in the current session
- `/mcp list` to see which config file a server came from
- `/mcp test <name>` to test a single server
## Validation rules OMP enforces
From `validateServerConfig()` in `packages/coding-agent/src/mcp/config.ts`:
- `stdio` requires `command`
- `http` and `sse` require `url`
- a server cannot set both `command` and `url`
- unknown `type` values are rejected
Practical implications:
- Omitting `type` means `stdio`
- If you paste a remote server config and forget `"type": "http"`, OMP will treat it as `stdio` and complain that `command` is missing
- `sse` remains valid for compatibility, but new hosted servers should usually be configured as `http`
## Discovery and precedence
OMP does not merge duplicate server definitions across files. Discovery providers are prioritized, and the higher-priority definition wins.
In practice:
- prefer `.omp/mcp.json` or `~/.omp/mcp.json` when you want an OMP-specific override
- keep server names unique across tools when possible
- use `disabledServers` in the user config when a third-party config keeps reintroducing a server you do not want
## Troubleshooting
### `Server "name": stdio server requires "command" field`
You probably omitted `type: "http"` on a remote server.
### `Server "name": both "command" and "url" are set`
Pick one transport. OMP treats `command` as stdio and `url` as http/sse.
### `/mcp add` worked but the server still does not connect
The JSON is valid, but the server may still be unreachable. Use `/mcp test <name>` and check whether:
- the binary or Docker image exists
- required environment variables are set
- the remote URL is reachable
- the OAuth or API token is valid
### The server exists in another tool's config but not in OMP
Run `/mcp list`. OMP discovers many third-party MCP files, but project-level loading can also be disabled via the `mcp.enableProjectConfig` setting.
## References
- MCP transport spec: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
- Filesystem server package: https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem
- GitHub MCP server: https://github.com/github/github-mcp-server
- Slack MCP server docs: https://docs.slack.dev/ai/slack-mcp-server/
+3 -1
View File
@@ -8,5 +8,7 @@ For installation, setup, provider configuration, model roles, slash commands, an
Package-specific references:
- [CHANGELOG](./CHANGELOG.md)
- [Documentation](./docs/)
- [MCP configuration guide](../../docs/mcp-config.md)
- [MCP runtime lifecycle](../../docs/mcp-runtime-lifecycle.md)
- [MCP server/tool authoring](../../docs/mcp-server-tool-authoring.md)
- [DEVELOPMENT](./DEVELOPMENT.md)
@@ -23,6 +23,8 @@ export interface MCPServer {
args?: string[];
/** Environment variables */
env?: Record<string, string>;
/** Working directory for stdio transport */
cwd?: string;
/** URL (for HTTP/SSE transport) */
url?: string;
/** HTTP headers (for HTTP transport) */
@@ -0,0 +1,230 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
"title": "OMP MCP configuration",
"description": "Schema for mcp.json, .mcp.json, .omp/mcp.json, and ~/.omp/mcp.json used by the OMP coding agent.",
"type": "object",
"additionalProperties": false,
"properties": {
"$schema": {
"type": "string",
"description": "Optional schema reference for editor autocomplete and validation."
},
"mcpServers": {
"type": "object",
"description": "Map of MCP server name to server configuration.",
"propertyNames": {
"pattern": "^[a-zA-Z0-9_.-]{1,100}$"
},
"additionalProperties": {
"$ref": "#/$defs/serverConfig"
}
},
"disabledServers": {
"type": "array",
"description": "User-level denylist for disabling discovered servers by name.",
"items": {
"type": "string",
"minLength": 1
},
"uniqueItems": true
}
},
"$defs": {
"stringMap": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"authConfig": {
"type": "object",
"additionalProperties": false,
"required": ["type"],
"properties": {
"type": {
"type": "string",
"enum": ["oauth", "apikey"],
"description": "Auth strategy understood by OMP."
},
"credentialId": {
"type": "string",
"description": "Stored OAuth credential id from agent auth storage."
},
"tokenUrl": {
"type": "string",
"description": "Token endpoint persisted for refresh."
},
"clientId": {
"type": "string",
"description": "OAuth client id persisted for refresh."
},
"clientSecret": {
"type": "string",
"description": "OAuth client secret persisted for refresh."
}
}
},
"oauthConfig": {
"type": "object",
"additionalProperties": false,
"properties": {
"clientId": {
"type": "string"
},
"clientSecret": {
"type": "string"
},
"redirectUri": {
"type": "string"
},
"callbackPort": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"callbackPath": {
"type": "string"
}
},
"description": "Explicit OAuth client settings for servers that need them during /mcp reauth or initial connect."
},
"serverBase": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether OMP should try to connect this server."
},
"timeout": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Connection timeout in milliseconds."
},
"auth": {
"$ref": "#/$defs/authConfig"
},
"oauth": {
"$ref": "#/$defs/oauthConfig"
}
}
},
"stdioServer": {
"allOf": [
{
"$ref": "#/$defs/serverBase"
},
{
"type": "object",
"additionalProperties": false,
"required": ["command"],
"properties": {
"type": {
"type": "string",
"enum": ["stdio"],
"description": "Default transport when omitted."
},
"command": {
"type": "string",
"minLength": 1,
"description": "Executable to spawn."
},
"args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Arguments passed to the stdio server process."
},
"env": {
"$ref": "#/$defs/stringMap",
"description": "Environment variables passed to the stdio process."
},
"cwd": {
"type": "string",
"description": "Working directory used when spawning the stdio process."
}
},
"not": {
"required": ["url"]
}
}
]
},
"httpServer": {
"allOf": [
{
"$ref": "#/$defs/serverBase"
},
{
"type": "object",
"additionalProperties": false,
"required": ["type", "url"],
"properties": {
"type": {
"type": "string",
"enum": ["http"],
"description": "Streamable HTTP transport."
},
"url": {
"type": "string",
"minLength": 1,
"description": "MCP endpoint URL."
},
"headers": {
"$ref": "#/$defs/stringMap",
"description": "HTTP headers sent with MCP requests."
}
},
"not": {
"required": ["command"]
}
}
]
},
"sseServer": {
"allOf": [
{
"$ref": "#/$defs/serverBase"
},
{
"type": "object",
"additionalProperties": false,
"required": ["type", "url"],
"properties": {
"type": {
"type": "string",
"enum": ["sse"],
"description": "Legacy SSE transport kept for compatibility. Prefer http for new configs."
},
"url": {
"type": "string",
"minLength": 1,
"description": "Legacy SSE endpoint URL."
},
"headers": {
"$ref": "#/$defs/stringMap",
"description": "HTTP headers sent with the SSE transport."
}
},
"not": {
"required": ["command"]
}
}
]
},
"serverConfig": {
"oneOf": [
{
"$ref": "#/$defs/stdioServer"
},
{
"$ref": "#/$defs/httpServer"
},
{
"$ref": "#/$defs/sseServer"
}
]
}
}
}
@@ -158,6 +158,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
command: serverConfig.command as string | undefined,
args: serverConfig.args as string[] | undefined,
env: serverConfig.env as Record<string, string> | undefined,
cwd: serverConfig.cwd as string | undefined,
url: serverConfig.url as string | undefined,
headers: serverConfig.headers as Record<string, string> | undefined,
auth: serverConfig.auth as
@@ -29,6 +29,7 @@ interface MCPConfigFile {
command?: string;
args?: string[];
env?: Record<string, string>;
cwd?: string;
url?: string;
headers?: Record<string, string>;
auth?: {
@@ -88,6 +89,7 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta): MCPServe
command: serverConfig.command,
args: serverConfig.args,
env: serverConfig.env,
cwd: serverConfig.cwd,
url: serverConfig.url,
headers: serverConfig.headers,
auth: serverConfig.auth,
@@ -100,6 +102,7 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta): MCPServe
if (server.command) server.command = expandEnvVarsDeep(server.command);
if (server.args) server.args = expandEnvVarsDeep(server.args);
if (server.env) server.env = expandEnvVarsDeep(server.env);
if (server.cwd) server.cwd = expandEnvVarsDeep(server.cwd);
if (server.url) server.url = expandEnvVarsDeep(server.url);
if (server.headers) server.headers = expandEnvVarsDeep(server.headers);
if (server.auth) server.auth = expandEnvVarsDeep(server.auth);
@@ -9,7 +9,14 @@ import { isEnoent } from "@oh-my-pi/pi-utils";
import { invalidate as invalidateFsCache } from "../capability/fs";
import { validateServerConfig } from "./config";
import type { MCPConfigFile, MCPServerConfig } from "./types";
import { MCP_CONFIG_SCHEMA_URL, type MCPConfigFile, type MCPServerConfig } from "./types";
function withSchema(config: MCPConfigFile): MCPConfigFile {
return {
$schema: config.$schema ?? MCP_CONFIG_SCHEMA_URL,
...config,
};
}
/**
* Read an MCP config file.
@@ -40,7 +47,7 @@ export async function writeMCPConfigFile(filePath: string, config: MCPConfigFile
// Write to temp file first (atomic write)
const tmpPath = `${filePath}.tmp`;
const content = JSON.stringify(config, null, 2);
const content = JSON.stringify(withSchema(config), null, 2);
await fs.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
// Rename to final path (atomic on most systems)
+1
View File
@@ -53,6 +53,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
};
if (server.args) config.args = server.args;
if (server.env) config.env = server.env;
if (server.cwd) config.cwd = server.cwd;
return config;
}
+5 -1
View File
@@ -100,8 +100,12 @@ export interface MCPSseServerConfig extends MCPServerConfigBase {
export type MCPServerConfig = MCPStdioServerConfig | MCPHttpServerConfig | MCPSseServerConfig;
/** Root .mcp.json file structure */
export const MCP_CONFIG_SCHEMA_URL =
"https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json";
/** Root mcp.json/.mcp.json file structure */
export interface MCPConfigFile {
$schema?: string;
mcpServers?: Record<string, MCPServerConfig>;
disabledServers?: string[];
}