Skip to main content
Toolshed ships two MCP server surfaces, with different tool sets tuned for the different transports.
TransportWhere it livesAuthTools exposed
stdio (toolshed serve)CLI command, local-onlyLocal session tokenrun, search_tools, read_tool, resume
HTTP (POST /api/mcp/:token)Remote connector for Claude Desktop, web agentsMCP token in URLsearch_tools, read_tool, call_tool
Both surfaces front the same plugin catalog. They differ in how the agent executes tools:
  • stdio path lets the agent compose multiple calls inside a TypeScript sandbox (run) and pause for user approval mid-execution (resume).
  • HTTP path (used by Claude Desktop’s “Add custom connector”) executes one tool per call via call_tool. There’s no sandbox; approval happens upstream in the host UI.

HTTP transport details

The remote endpoint at POST /api/mcp/:token uses Streamable HTTP transport (@hono/mcp’s StreamableHTTPTransport). Sessions are per-MCP-token and survive across requests:
  1. First request: initialize JSON-RPC message. Server generates a session id and returns it in the mcp-session-id response header.
  2. Subsequent requests: client must echo the session id in the mcp-session-id request header.
  3. Each session caches a per-user McpServer instance with availableTools already filtered (see below).
Per-user tool filtering. When a session is created, the server computes which auth providers the user has connected (union of tool_connections rows, Better Auth account rows for OAuth, and env-backed API keys for built-ins). search_tools and read_tool only operate over tools whose effective authProvider is in that set, so the agent never sees a tool it can’t actually call. Token resolution. When call_tool runs a plugin handler, the plugin calls ctx.auth.getToken("<provider>"). The server’s resolveToken() checks three sources in order: per-user tool_connections row → Better Auth’s stored OAuth token (with auto-refresh) → process.env["${UPPER}_API_KEY"] for built-in tools.

stdio transport details

The CLI’s toolshed serve command runs an MCP server over stdio via @toolshed/mcp-host. It registers the four tools listed above against the runtime (local or Vercel) and the full tool catalog:
import { createMcpServer } from "@toolshed/mcp-host";

const server = createMcpServer({
  runtime,        // Runtime instance (local or Vercel)
  allTools,       // Full tool catalog
  searchTools,    // (query, limit) => ToolDefinition[]
  elicitation,    // Optional ElicitationEngine
  resumeRemote,   // Optional remote resume callback
});
The stdio surface does not filter by connected providers — the CLI flow assumes the local user has whatever credentials they need. Same pattern on both transports:
  1. Discover — call search_tools with a keyword to find relevant tools.
  2. Inspect — call read_tool to see the full input/output schema.
  3. Executecall_tool (HTTP) or run (stdio) to invoke the chosen tool.
  4. Approve — only on stdio: if the result includes a paused field, call resume.