MCPico
by lxg2it
README.md
# MCPico
**MCP proxy that bundles flat tool lists into hierarchical groups with separate discovery and execution.**
MCPico (MCP + "ico" = tiny) wraps upstream MCP servers, grouping their tools into discoverable groups. Each group gets a `help_<group>` discovery tool (auto-generated docs from upstream schemas) and a `<group>` execution tool. LLM benchmarks show 43–60% fewer conversation tokens while matching flat tool success rates.
## The Problem
MCP servers expose tools as a flat list. Every tool costs context tokens. A filesystem server exposes 14+ separate tools — the model sees all of them, all the time, even when it only needs one.
Some clients add "tool search" as a workaround. But searching requires the model to proactively look for tools it doesn't know exist. No structural signal about which tools relate to each other.
## MCPico's Solution
Group related tools under a single entry point. The model sees groups instead of raw tools. Discovery is separated from execution:
```
Model calls: help_postgres → sees available tools
Model calls: postgres_query {"sql":"SELECT ..."} → executes
```
### Quantified: 43–60% fewer conversation tokens
See **[BENCHMARK.md](BENCHMARK.md)** for a full LLM evaluation comparing flat tools (45 tools, 5 servers), MCPico merged mode, and MCPico split mode across Qwen3.5-9B and Qwen3.6-35B.
Key results:
- **MCPico split** matches flat tool success rates on both models (2/3 tasks)
- **60% token reduction** on 9B model (14,027 vs 34,760 tokens across all tasks)
- **43% token reduction** on single-tool tasks with the 35B model
## Features
- **Tool bundling** — Groups tools by prefix (configurable separator), collapsing flat tool lists into 10 tools instead of 45+
- **Split discovery/execution** — Separate `help_<group>` tools for discovery, `<group>` tools for execution. LLM-optimized design
- **Auto-generated help** — `help_<group>` tools dynamically generate rich documentation from upstream schemas
- **Multi-server aggregation** — Proxy multiple upstream MCP servers through one interface
- **Dual upstream transport** — Supports both stdio and Streamable HTTP (SSE) upstream servers
- **Dual listen transport** — MCPico itself listens via stdio or HTTP/SSE (configurable port)
- **Configurable timeouts** — Per-server connection timeout with sensible default (30s)
- **Resource & prompt passthrough** — Namespaced to avoid collisions across servers
- **Authentication** — Bearer, custom header, and OAuth2 client_credentials with automatic token refresh
- **Listen endpoint auth** — Protect the SSE endpoint with bearer token validation
## Usage
### Install
```bash
npm install -g mcpico
```
### Configure
Create `mcpico.json`:
```json
{
"servers": [
{
"name": "filesystem",
"transport": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"]
}
}
]
}
```
### Run
```bash
mcpico
```
### Connect your MCP client
Add MCPico as a server in your MCP client config:
```json
{
"mcpServers": {
"mcpico": {
"command": "mcpico",
"args": ["--config", "/path/to/mcpico.json"]
}
}
}
```
## How it works
1. **Connect** to upstream MCP servers
2. **Discover** their tools (`tools/list`)
3. **Group** tools by prefix (configurable separator, default `_`)
- `filesystem_read_file`, `filesystem_write_file` → group `filesystem`
4. **Register** two tools per group:
- `help_<group>` — discovery: lists all subcommands with their parameters
- `<group>` — execution: takes `subcommand` + `params`, forwards to upstream
5. **Forward** tool calls directly to the matching upstream server
6. **Generate help** dynamically from original tool schemas
### Tool interface
```
help_postgres ← call with no arguments to discover
postgres ← call with subcommand: "postgres_query", params: {sql: "..."}
```
### Multi-server aggregation
MCPico can proxy multiple upstream servers simultaneously:
```json
{
"servers": [
{
"name": "filesystem",
"transport": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
},
{
"name": "github",
"transport": {
"type": "sse",
"url": "https://mcp-github.example.com/mcp"
}
}
]
}
```
Groups from different servers are merged if they share a prefix. Otherwise each server's tools appear as separate groups.
## Configuration
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `servers` | `ServerConfig[]` | **required** | Upstream MCP servers to proxy |
| `separator` | `string` | `"_"` | Separator for prefix-based tool grouping |
| `groups` | `object` | `{}` | Explicit group overrides (`{ "group": ["tool1","tool2"] }`) |
| `listen` | `ListenConfig` | `{"type":"stdio"}` | How MCPico exposes itself to MCP clients |
### ListenConfig
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | `"stdio"` | yes | Standard stdio transport |
| `type` | `"sse"` | yes | HTTP/SSE — specify `port` and optional `host` |
```json
// SSE listen mode — MCPico as an HTTP endpoint
{
"servers": [...],
"listen": {
"type": "sse",
"port": 3000
}
}
```
### ServerConfig
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | yes | Friendly name / group namespace |
| `transport` | `TransportConfig` | yes | How to connect to the upstream server |
| `connectTimeoutMs` | `number` | no | Connection timeout in ms (default: 30000) |
### TransportConfig (stdio)
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | `"stdio"` | yes | Transport type |
| `command` | `string` | yes | Executable to spawn |
| `args` | `string[]` | no | Command-line arguments |
| `env` | `object` | no | Environment variables |
| `cwd` | `string` | no | Working directory |
### TransportConfig (SSE / Streamable HTTP)
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | `"sse"` | yes | Transport type |
| `url` | `string` | yes | Full URL to MCP Streamable HTTP endpoint |
## Authentication
MCPico supports two layers of authentication:
### Layer 1: Protecting the listen endpoint
When MCPico exposes an SSE endpoint, you can require a bearer token from clients:
```json
{
"servers": [...],
"listen": {
"type": "sse",
"port": 3000,
"auth": {
"type": "bearer",
"token": "${MCPICO_API_KEY}"
}
}
}
```
Clients must include `Authorization: Bearer <token>` in requests. Invalid or missing tokens receive a 401 response.
### Layer 2: Authenticating to upstream servers
Upstream servers can require authentication. MCPico supports three methods:
**Bearer token** — standard `Authorization: Bearer <token>` header:
```json
{
"servers": [
{
"name": "internal-api",
"transport": {
"type": "sse",
"url": "https://api.internal/mcp"
},
"auth": {
"type": "bearer",
"token": "${INTERNAL_KEY}"
}
}
]
}
```
**Custom header** — arbitrary headers (e.g. `X-API-Key`):
```json
{
"auth": {
"type": "header",
"name": "X-API-Key",
"value": "${WIDGET_KEY}"
}
}
```
**OAuth 2.0 client credentials** — machine-to-machine authentication with automatic token refresh:
```json
{
"auth": {
"type": "oauth",
"grant_type": "client_credentials",
"client_id": "${PROVIDER_CLIENT_ID}",
"client_secret": "${PROVIDER_CLIENT_SECRET}",
"token_url": "https://auth.example.com/oauth/token",
"scopes": ["read", "write"]
}
}
```
MCPico handles the full OAuth flow:
- Fetches initial access token on startup
- Caches tokens in `~/.mcplico/credentials.json`
- Automatically refreshes before expiry
- Retries on 401 with fresh tokens
All auth fields support `${ENV_VAR}` interpolation — never hardcode secrets.
### Auth config reference
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `auth.type` | `"bearer"` \| `"header"` \| `"oauth"` | yes | Auth method |
| `auth.token` | `string` | for `bearer` | Bearer token value |
| `auth.name` | `string` | for `header` | Header name |
| `auth.value` | `string` | for `header` | Header value |
| `auth.grant_type` | `"client_credentials"` | for `oauth` | OAuth grant type |
| `auth.client_id` | `string` | for `oauth` | OAuth client ID |
| `auth.client_secret` | `string` | for `oauth` | OAuth client secret |
| `auth.token_url` | `string` | for `oauth` | Token endpoint URL |
| `auth.scopes` | `string[]` | no | OAuth scopes to request |
| `auth.authorization_server_url` | `string` | no | Auth server URL (if different from token_url issuer) |
## Development
## Development
```bash
npm install
npm run build # TypeScript compilation
npm test # Run tests (138 tests, vitest)
npm run dev # Run directly with tsx
```
## License
MIT
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessUnresponsive