msp is a small Rust CLI that lets an AI work with many MCP servers through one proxy server.
Instead of exposing every downstream MCP tool directly, msp exposes a small built-in proxy toolset. This keeps the upstream tool list small, reduces prompt noise, and avoids wasting tokens on tools the agent will never use.
The installed binary name is msp. Running msp without arguments shows the top-level help.
- Reduce the number of tools your agent sees (reduce the token cost without losing any tool).
- Cache downstream MCP tool metadata and summaries.
- Proxy both local stdio MCP servers and remote Streamable HTTP MCP servers.
- Reuse your existing Codex, OpenCode, Claude Code, or GitHub Copilot CLI MCP setup instead of rebuilding everything from scratch.
msp does three things:
- Connects to each configured MCP server and caches its tool metadata.
- Generates a short summary for each server by using a configured provider:
codex,opencode, orclaude, or uses a manually configured server description. - Starts a stdio MCP proxy that exposes these proxy tools:
activate_additional_mcpsactivate_tools_in_additional_mcpcall_tool_in_additional_mcpeval_lua_script
Agents first inspect the cached server index, optionally inspect one tool definition, and then call the downstream tool through the proxy.
The built-in eval_lua_script tool runs Lua 5.5 scripts inside the proxy through mlua. Scripts can call any configured downstream MCP tool through call_mcp_tool(mcp_name, tool_name, args), where args is a Lua table that maps to a JSON object.
When a host starts msp mcp, msp auto-starts one background daemon for that config file. That daemon owns downstream MCP communication and periodic self-update checks. Later msp mcp processes that use the same config reuse the same Unix socket daemon, even when they pass different --provider values. The daemon exits after 1 hour with no requests.
The default daemon socket lives under ~/.cache/mcp-smart-proxy/ and uses a short hash of the config path so it stays within Unix socket path limits on macOS and Linux.
curlorwget, plustar, for installation- The
codexCLI when using--provider codex - The
opencodeCLI when using--provider opencode - The
claudeCLI when using--provider claude - The
copilotCLI when using--provider copilot - A browser session for remote MCP servers that require OAuth login
Install the latest release:
curl -fsSL https://raw.githubusercontent.com/cybershape/mcp-smart-proxy/master/install.sh | bashInstall to a custom directory:
curl -fsSL https://raw.githubusercontent.com/cybershape/mcp-smart-proxy/master/install.sh | INSTALL_DIR=/tmp/msp/bin bashInstall a specific version:
curl -fsSL https://raw.githubusercontent.com/cybershape/mcp-smart-proxy/master/install.sh | VERSION=v0.0.19 bashBy default the installer writes msp to:
- macOS on Apple Silicon:
/opt/homebrew/bin - macOS on Intel:
/usr/local/bin - Linux as root:
/usr/local/bin - Linux as a regular user:
~/.local/bin
After installation:
mspImport your existing Codex MCP servers into msp, replace Codex's MCP entries with the proxy, and keep a backup:
msp import codex --replaceIf you want to restore the original Codex MCP servers later:
msp restore codexTo add a new server after that:
msp add --provider codex github npx -y @modelcontextprotocol/server-githubTo add a new server with a manual summary and no AI provider:
msp add --description "Use this for GitHub workflows." github npx -y @modelcontextprotocol/server-githubTo add a remote server that needs headers up front:
msp add --provider codex --url https://example.com/mcp --header Authorization='Bearer ${DEMO_TOKEN}' remote-demoImport existing servers:
msp import opencode
msp import claude
msp import copilotInstall the proxy into the host:
msp install opencode
msp install claude
msp install copilotInstall the bundled pi extension globally:
msp install piReplace existing host MCP entries and keep a backup:
msp install opencode --replace
msp install claude --replace
msp install copilot --replaceRestore the original host config later if needed:
msp restore opencode
msp restore claude
msp restore copilotRemove the global pi extension later if needed:
msp restore piAdd a server:
msp add --provider codex github npx -y @modelcontextprotocol/server-githubInstall msp into your host:
msp install codexFrom that point, the host launches msp mcp as its MCP server entrypoint. Pass --provider <provider> when you want msp to regenerate server summaries with that provider.
The first launch starts the shared daemon automatically. Later launches for the same config reuse it.
You can inspect or control that shared process directly:
msp daemon status
msp daemon stop
msp daemon restartmsp add --provider codex github npx -y @modelcontextprotocol/server-githubOr skip AI summarization and persist a manual description instead:
msp add --description "Use this for GitHub workflows." github npx -y @modelcontextprotocol/server-githubIf the command is a single http:// or https:// URL, msp stores it as a native remote server:
msp add --provider codex remote-demo https://example.com/mcpIf the server needs headers, env values, forwarded env vars, or an initial enabled state, pass them during add so the first cache refresh uses the final connection settings:
msp add --provider codex --url https://example.com/mcp --header Authorization='Bearer ${DEMO_TOKEN}' --env DEMO_REGION=global --env-var DEMO_TOKEN --enabled false remote-demoadd requires either --provider or --description. With --provider, msp summarizes the fetched tools immediately. With --description, msp stores that text in the local config and uses it as the cached server summary without calling any AI provider. If both are passed, the manual description wins. The command succeeds only when both config persistence and the initial cache refresh succeed; if cache generation fails, msp rolls back the new server entry instead of leaving partial config behind. When you use config flags with add, place them before the server name so the trailing command remains untouched. You can still refresh later with msp reload, or let the shared msp mcp daemon refresh enabled servers in the background after startup.
msp listmsp list shows each configured server, whether it is enabled, and when its cache was last refreshed.
msp disable github
msp enable githubDisabled servers stay in the config and keep their cache files, but bulk reload and daemon-managed mcp startup skip them.
Show current config:
msp config githubUpdate a stdio server:
msp config github --cmd uvx --clear-args --arg demo-server --env DEMO_REGION=global --env-var DEMO_TOKEN --enabled falseUpdate a remote server:
msp config remote-demo --url https://example.com/mcp --clear-headers --header Authorization='Bearer ${DEMO_TOKEN}'Forward one shell variable into a remote server config:
msp config remote-demo --env-var DEMO_TOKENmsp config can update transport, enabled, command or URL fields, headers, static env values, and forwarded env var names.
Reload one server:
msp reload --provider codex githubReload every enabled server:
msp reload --provider codexreload fetches the downstream tool list, compares it to the cache, and only regenerates the summary when the tool list changed. If you omit --provider, reload still updates tools and keeps the existing cached summary unless the server has a manual description in config.
The built-in eval_lua_script MCP tool is useful when the host wants lightweight programmable orchestration without exposing every downstream tool directly in the prompt.
Example Lua script:
local docs = call_mcp_tool("context7", "query-docs", {
libraryId = "/websites/rs_crate_mlua",
query = "create_async_function examples",
})
return {
isError = docs.isError,
structured = docs.structuredContent,
}eval_lua_script returns structured JSON. A script that returns no values produces null, one value produces that value, and multiple return values are encoded as a JSON array.
Start OAuth login:
msp login remote-demoClear cached OAuth credentials:
msp logout remote-demoOAuth metadata is discovered from the remote MCP server at runtime. Credentials are cached under ~/.cache/mcp-smart-proxy/oauth/.
msp does not support Figma's hosted MCP endpoint at https://mcp.figma.com/mcp.
The proxy rejects that URL during msp add ..., msp config --url, and local config load with a clear error instead of letting setup continue into a broken OAuth flow.
During msp import ... and msp install ... --replace, Figma hosted MCP entries are skipped for import and left in the original host config instead of being deleted.
msp add --provider codex figma https://mcp.figma.com/mcpExpected result:
server `figma` uses unsupported remote MCP URL `https://mcp.figma.com/mcp`; msp does not support Figma's hosted MCP endpoint
msp remove githubThis removes the server from the config and deletes its cached tool file if one exists.
msp updateThis checks GitHub Releases, downloads the newest build for the current platform, and replaces the current executable in place when a newer release exists.
When the daemon is running, it is also responsible for periodic background self-update checks for msp mcp traffic.
Check whether the daemon for the current config is running:
msp daemon statusStop the daemon and remove its socket and pid state:
msp daemon stopRestart the daemon for the current config:
msp daemon restartAll three commands also accept --socket <path> when you need to target a custom daemon socket.
Keep custom socket paths short enough for Unix domain socket limits.
The daemon writes a runtime log next to its socket, for example ~/.cache/mcp-smart-proxy/msp-<scope>.sock.log.
Detached daemon startup also writes ~/.cache/mcp-smart-proxy/msp-<scope>.sock.startup.log until the new daemon answers a status probe. If startup fails or the new daemon becomes unresponsive before it can serve status, that startup log is kept for diagnosis.
If the daemon socket accepts a connection but never replies, msp daemon status, stop, and restart fail quickly instead of hanging indefinitely. stop and restart also fall back to force-stopping the unresponsive daemon by pid-file state so the socket can recover without manual cleanup.
Concurrent daemon refresh requests for the same provider are coalesced into one shared reload. Slow cache-lock waits, MCP tool discovery, and summary subprocesses also fail with timeouts instead of blocking the daemon forever.
Install the proxy into Codex, OpenCode, Claude Code, or GitHub Copilot CLI:
msp install codex
msp install opencode
msp install claude
msp install copilotInstall the bundled global pi extension:
msp install piThis writes the embedded extension source to ~/.pi/agent/extensions/msp.ts.
With --replace, msp first imports the host's current MCP servers into msp, backs them up, removes them from the host config, and then installs the proxy:
msp install codex --replace
msp install opencode --replace
msp install claude --replace
msp install copilot --replaceBackup files:
- Codex:
$CODEX_HOME/config.msp-backup.tomlor~/.codex/config.msp-backup.toml - OpenCode:
~/.config/opencode/opencode.msp-backup.json - Claude Code:
~/.claude.msp-backup.json
Restore host config from backup:
msp restore codex
msp restore opencode
msp restore claude
msp restore copilotRemove the bundled global pi extension:
msp restore pimsp can import MCP servers from:
- Codex:
msp import codex - OpenCode:
msp import opencode - Claude Code:
msp import claude
Provider selection works like this:
import codexdefaults to providercodeximport opencodedefaults to provideropencodeimport claudedefaults to providerclaude--provider ...overrides the default summary provider
Examples:
msp import codex
msp import --provider opencode codex
msp import opencode
msp import claudeImport behavior:
- Existing names are skipped after normalization.
- Entries that launch
msp mcpare skipped. - Only supported MCP config shapes are imported.
- If refresh fails during the import batch,
msprolls back the servers added in that run.
Default config path:
~/.config/mcp-smart-proxy/config.toml
Override it with --config <PATH>.
The default daemon socket path is derived from this config path with a short stable hash, so different config files still get distinct daemons without exceeding Unix socket length limits.
Example:
[servers.github]
transport = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
description = "Use this for GitHub workflows."
enabled = true
env_vars = ["GITHUB_TOKEN"]
[servers.github.env]
GITHUB_API_URL = "https://api.github.com"
GITHUB_ENTERPRISE_MODE = "false"
[servers.test]
transport = "remote"
url = "https://example.com/mcp"
enabled = false
env_vars = ["DEMO_TOKEN", "DEMO_REGION"]
[servers.test.headers]
Authorization = "Bearer ${DEMO_TOKEN}"
X-Region = "${DEMO_REGION:-global}"
[servers.filesystem]
transport = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]Notes:
- The config file stores managed
serversonly. - Servers are enabled by default unless
enabled = false. transportis optional. If omitted,mspinfers it fromcommandorurl.stdioservers usecommandplusargs.remoteservers use a Streamable HTTPurlplus optionalheaders.descriptionis an optional manual summary that overrides AI-generated summaries for that server.envstores static variables for the downstream server.env_varslists variables thatmspforwards from its own environment.
msp mcp --provider codexmsp mcp is a stdio MCP server entrypoint, not an interactive shell command. Start it from an MCP host such as Codex, OpenCode, or Claude Code, or install it with msp install ....
If you want downstream tools that return structuredContent to expose compact TOON text instead, start the proxy with --output-toon. In that mode, call_tool_in_additional_mcp converts the downstream structured JSON to TOON text, replaces the text content with that TOON payload, and clears structuredContent before returning the result upstream.
When the proxy starts, msp mcp serves the currently cached toolsets immediately and asks the shared daemon to refresh every enabled configured server in the background. If you pass --provider ..., that refresh also regenerates summaries with the selected provider. If you omit --provider, the daemon still refreshes tools and keeps each server's manual description or existing cached summary. The current stdio session keeps using the startup cache snapshot; refreshed cache is used by later sessions or explicit reloads. Background refresh failures are logged by the daemon and do not block MCP readiness.
msp cli gives you an interactive terminal view over the same cached MCP inventory that the proxy uses.
List cached MCP servers and their summaries:
msp cli -hList cached tools for one MCP server:
msp cli github -hShow one tool's argument help:
msp cli github search_repositories -hCall one downstream MCP tool through the shared daemon:
msp cli github search_repositories --query rust --page 1If you want terminal output in TOON when a downstream tool returns structuredContent, add --output-toon before the MCP name:
msp cli --output-toon github search_repositories --query rust --page 1msp cli starts or reuses the shared daemon, loads the current cached MCP snapshot through the daemon protocol, and routes the final downstream tool call through the daemon as well.
This repository also includes a project-local pi extension in ./pi-extension.
It detects whether msp is available on the local system, runs msp cli -h once per pi session, caches that output, and appends the cached inventory plus short MSP usage guidance to pi's systemPrompt. That gives pi a lightweight snapshot of which cached MCP servers are currently reachable through msp cli.
The same extension source is embedded in the msp binary, so you can install it globally without copying files out of this repository:
msp install piThat writes ~/.pi/agent/extensions/msp.ts. If that file already exists, msp install pi overwrites it.
Try the project-local copy without installing anything:
pi -e ./pi-extension/msp.tsYou can still copy or symlink ./pi-extension into .pi/extensions/ or ~/.pi/agent/extensions/ for local development if you want pi to auto-discover it directly from the repository.
If msp is missing or msp cli -h fails, the extension shows one warning and then skips prompt injection for that session.
When your MSP inventory changes, run /reload or start a new pi session so the extension refreshes its cached msp cli -h snapshot. If you installed the global extension with msp install pi, later msp self-updates also refresh that installed ~/.pi/agent/extensions/msp.ts automatically when the bundled extension changes.
When msp mcp is running, it checks GitHub Releases every 30 minutes.
- If a newer build exists for the current platform, it downloads and atomically replaces the current
mspbinary. - It writes a sibling latest-version record next to the binary.
- If the running process sees that it is older than that record, it restarts itself into the updated binary.
- If
~/.pi/agent/extensions/msp.tsalready exists, the restarted binary refreshes that installed global pi extension automatically when the bundled extension changed. - Lock files prevent concurrent updates from racing on the same executable path.
Background self-update requires write access to the installed msp binary path.
Input:
{
"external_mcp_names": ["github", "filesystem"]
}Output:
[github]
example_tool: Example description
another_tool: Another description that is longer but still fits in the preview
[filesystem]
read_file: Read the contents of a file
Each section starts with [mcp_name]. Within a section, each output line is tool_name: description-preview. If a tool has no description, the line is just tool_name.
Input:
{
"external_mcp_name": "github",
"tool_names": ["example_tool", "another_tool"]
}Output:
{
"tools": [
{
"name": "example_tool",
"title": "Example Tool",
"description": "Example description",
"input_schema": {}
},
{
"name": "another_tool",
"title": "Another Tool",
"description": "Another example",
"input_schema": {}
}
]
}Input:
{
"external_mcp_name": "github",
"tool_name": "example_tool",
"args_in_json": "{\"owner\":\"octo-org\",\"repo\":\"demo\"}"
}args_in_json must decode to a JSON object or null.
- Downstream MCP servers must use either stdio or Streamable HTTP transport.
- Remote OAuth currently assumes an interactive browser-based authorization code flow.
- Tool discovery depends on metadata cached by
reload. - The proxy exposes a fixed activation-and-call interface instead of dynamically re-exporting downstream tools as first-class proxy tools.