Skip to content

MCP configuration in pisces

This guide explains how to add, edit, and validate MCP servers for the pisces 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

pisces can discover MCP servers from multiple tools (.claude/, .cursor/, .vscode/, opencode.json, and more), but for pisces-native configuration you should usually use one of these files:

  • Project: .pisces/mcp.json
  • User: ~/.pisces/mcp.json

pisces also accepts fallback standalone files in the project root:

  • mcp.json
  • .mcp.json

Use .pisces/mcp.json when you want pisces 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": {}
}

pisces now writes this automatically when /mcp add, /mcp enable, /mcp disable, /mcp reauth, or other config-writing flows create or update a pisces-managed MCP file.

File shape

pisces 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 pisces 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

pisces 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 pisces 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 .pisces/mcp.json and ~/.pisces/mcp.json

Before pisces launches a server or makes an HTTP request, it resolves env and headers values like this:

  1. If a value starts with !, pisces runs it as a shell command and uses trimmed stdout.
  2. Otherwise pisces first checks whether the value matches an environment variable name.
  3. If that environment variable is not set, pisces 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 pisces behavior, prefer .pisces/mcp.json and use explicit env/header values.

disabledServers

disabledServers is mainly useful in the user config file (~/.pisces/mcp.json) when a server is discovered from some other source and you want pisces 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 pisces 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", pisces 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

pisces does not merge duplicate server definitions across files. Discovery providers are prioritized, and the higher-priority definition wins.

In practice:

  • prefer .pisces/mcp.json or ~/.pisces/mcp.json when you want a pisces-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. pisces 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 pisces

Run /mcp list. pisces discovers many third-party MCP files, but project-level loading can also be disabled via the mcp.enableProjectConfig setting.

References