Runstack Docs

stdio MCP Server

Build a local stdio MCP server that runs as a subprocess. Best for local tooling, CLI scripts, and desktop clients like Claude Desktop.

The Runstack HTTP MCP server at /mcp is designed for remote access. A stdio MCP server is a different deployment model — it runs locally as a subprocess that your MCP client spawns on demand. Both speak the same MCP protocol; only the transport differs.

stdio vs HTTP

stdioHTTP (/mcp)
Transportstdin/stdout pipesJSON over HTTPS
Runs onUser's local machineRemote server
AuthProcess-level (no token needed)API key via Authorization header
Best forLocal scripts, Claude DesktopShared access, remote agents
StartupSpawned per sessionAlways-on endpoint

Use stdio when you want a lightweight local server that only runs when needed. Use HTTP when you need a shared, always-on endpoint (e.g. for a team or for remote MCP clients).

Prerequisites

  • Node.js 18+
  • @modelcontextprotocol/sdk package
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node

Minimal stdio server

// server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({
  name: 'my-local-server',
  version: '1.0.0',
});

server.registerTool(
  'say_hello',
  {
    description: 'Returns a greeting for the given name.',
    inputSchema: {
      name: z.string().describe('The name to greet'),
    },
  },
  async ({ name }) => ({
    content: [{ type: 'text', text: `Hello, ${name}!` }],
  }),
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  // Server now reads JSON-RPC from stdin and writes to stdout
}

main().catch(console.error);

Run it directly:

npx tsx server.ts

Connecting to Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "my-local-server": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/server.ts"]
    }
  }
}

Claude Desktop will spawn the process when it starts and communicate via stdin/stdout. No API key needed — auth is handled by the fact that only local processes can talk to it.

Connecting to Cursor

In .cursor/mcp.json:

{
  "my-local-server": {
    "command": "node",
    "args": ["/absolute/path/to/dist/server.js"]
  }
}

Accessing Runstack tools from a stdio server

You can call the Runstack HTTP MCP server from inside your stdio server using the MCP client SDK:

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const runstack = new Client({ name: 'runstack-client', version: '1.0.0' });

await runstack.connect(
  new StreamableHTTPClientTransport(
    new URL('https://app.runstack.engineer/mcp'),
    {
      requestInit: {
        headers: { Authorization: 'Bearer rsk_YOUR_API_KEY' },
      },
    },
  ),
);

// Now expose Runstack tools through your local stdio server
server.registerTool(
  'runstack_search',
  {
    description: 'Search Runstack tools',
    inputSchema: { pattern: z.string() },
  },
  async ({ pattern }) => {
    const result = await runstack.callTool({
      name: 'search_tools',
      arguments: { pattern },
    });
    return { content: result.content };
  },
);

Building for production

Add to package.json:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js"
  },
  "bin": {
    "my-server": "./dist/server.js"
  }
}

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "dist",
    "strict": true
  },
  "include": ["src"]
}

After npm run build, clients can use:

{
  "command": "node",
  "args": ["/path/to/dist/server.js"]
}

Or if installed globally via npm:

{
  "command": "my-server"
}

Best practices for stdio servers

  • Write logs to stderr, never stdout — stdout is reserved for the MCP protocol
  • Handle SIGTERM/SIGINT gracefully to close the transport cleanly
  • Keep startup fast — the client blocks until the server is ready
  • Avoid global state — each client spawns its own process instance

On this page