{"componentChunkName":"component---src-gatsby-theme-chronoblog-templates-note-js","path":"/notes/mcp-server-nodejs/","result":{"data":{"mdx":{"parent":{"__typename":"File","fields":{"gitLogLatestDate":"2026-06-06 16:03:49 +0200"}},"id":"d51e804e-6676-50e5-91db-6aacc23b59cb","excerpt":"The  Model Context Protocol (MCP)  is an open standard for connecting AI  hosts  (Claude, ChatGPT, Cursor, VS Code, and others) to external…","frontmatter":{"title":"Building an MCP server with Node.js","date":"2026-06-06 12:00:00 UTC","job_ad":null,"job_ad_id":null,"job_ad_url":null,"tags":["mcp","node","ai","claude","chatgpt"],"cover":null},"fields":{"slug":"/notes/mcp-server-nodejs/","readingTime":{"text":"7 min read"}},"body":"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"Building an MCP server with Node.js\",\n  \"date\": \"2026-06-06 12:00:00 UTC\",\n  \"tags\": [\"mcp\", \"node\", \"ai\", \"claude\", \"chatgpt\"],\n  \"canonical_url\": \"https://sevic.dev/notes/mcp-server-nodejs/\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, [\"components\"]);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"The \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://modelcontextprotocol.io/\"\n  }), \"Model Context Protocol (MCP)\"), \" is an open standard for connecting AI \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"hosts\"), \" (Claude, ChatGPT, Cursor, VS Code, and others) to external \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"context and actions\"), \" through a structured protocol instead of ad-hoc plugins.\"), mdx(\"p\", null, \"The host runs an \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"MCP client\"), \". Your application is the \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"MCP server\"), \", exposing \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"tools\"), \" (actions the model can invoke), \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"resources\"), \" (read-only data), and optionally \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"prompts\"), \" (reusable message templates). Communication uses JSON-RPC over a transport such as stdio or HTTP.\"), mdx(\"p\", null, \"This post shows how to build a small todo MCP server with Node.js using the official \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://www.npmjs.com/package/@modelcontextprotocol/sdk\"\n  }), mdx(\"inlineCode\", {\n    parentName: \"a\"\n  }, \"@modelcontextprotocol/sdk\")), \" package and \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://zod.dev/\"\n  }), \"Zod\"), \" schemas.\"), mdx(\"h3\", {\n    \"id\": \"architecture-at-a-glance\"\n  }, \"Architecture at a glance\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-mermaid\"\n  }), \"sequenceDiagram\\n  participant User\\n  participant Host as AI Host (Claude / ChatGPT)\\n  participant Client as MCP Client\\n  participant Transport as Transport (stdio or HTTP)\\n  participant Server as MCP Server (Node.js)\\n  participant Store as App data / APIs\\n\\n  User->>Host: Natural language request\\n  Host->>Client: Decide to call tool / read resource\\n  Client->>Transport: JSON-RPC (initialize, tools/list, tools/call)\\n  Transport->>Server: Deliver message\\n  Server->>Store: Read or mutate data\\n  Store-->>Server: Result\\n  Server-->>Transport: Tool result / resource contents\\n  Transport-->>Client: Response\\n  Client-->>Host: Structured result\\n  Host-->>User: Answer grounded in tool output\\n\")), mdx(\"p\", null, \"After the client connects, the server completes an initialization handshake (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"initialize\"), \" \\u2192 \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"initialized\"), \"). The host discovers capabilities via \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tools/list\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"resources/list\"), \", and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"prompts/list\"), \", then calls tools or reads resources at runtime.\"), mdx(\"h3\", {\n    \"id\": \"prerequisites\"\n  }, \"Prerequisites\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Node.js version 26\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"npm i @modelcontextprotocol/sdk zod\")), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Optional for client testing: Claude Desktop and/or ChatGPT (Connectors / Apps)\")), mdx(\"p\", null, \"The stable v1 SDK is \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"@modelcontextprotocol/sdk\"), \". A v2 split (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"@modelcontextprotocol/server\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"@modelcontextprotocol/client\"), \") is in pre-release - this post uses v1, which matches current production tooling.\"), mdx(\"h3\", {\n    \"id\": \"mcp-capabilities---tools-resources-prompts\"\n  }, \"MCP capabilities - tools, resources, prompts\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Capability\"), mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Purpose\"), mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Demo example\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"Tools\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Model-invoked actions with typed inputs\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"add_todo\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"list_todos\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"mark_todo_done\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"Resources\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Read-only context the host can fetch\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"todo://all\"), \" JSON snapshot\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"Prompts\"), \" (optional)\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Named templates with arguments\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"summarize-open-todos\"))))), mdx(\"p\", null, \"Tools are the main integration surface - the model calls them via \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tools/call\"), \". Resources are fetched with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"resources/read\"), \" and should stay read-only. Prompts return pre-built messages via \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"prompts/get\"), \".\"), mdx(\"p\", null, \"Tool inputs need a schema so clients know parameters. With the TypeScript SDK, pass Zod fields in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"inputSchema\"), \":\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\\nimport { z } from 'zod';\\n\\nconst server = new McpServer({ name: 'todo-mcp-server', version: '1.0.0' });\\n\\nserver.registerTool(\\n  'add_todo',\\n  {\\n    description: 'Add a new todo item',\\n    inputSchema: { title: z.string().min(1) },\\n  },\\n  async ({ title }) => ({\\n    content: [{ type: 'text', text: JSON.stringify({ title, done: false }) }],\\n  })\\n);\\n\")), mdx(\"p\", null, \"Register a resource at a fixed URI:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"server.registerResource(\\n  'all-todos',\\n  'todo://all',\\n  {\\n    title: 'All todos',\\n    mimeType: 'application/json',\\n  },\\n  async (uri) => ({\\n    contents: [\\n      {\\n        uri: uri.href,\\n        mimeType: 'application/json',\\n        text: JSON.stringify([{ id: 1, title: 'Learn MCP', done: false }]),\\n      },\\n    ],\\n  })\\n);\\n\")), mdx(\"p\", null, \"Register an optional prompt:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"server.registerPrompt(\\n  'summarize-open-todos',\\n  {\\n    title: 'Summarize open todos',\\n    description: 'Ask the model to summarize open todos',\\n  },\\n  () => ({\\n    messages: [\\n      {\\n        role: 'user',\\n        content: {\\n          type: 'text',\\n          text: 'Summarize my open todos and suggest a priority order.',\\n        },\\n      },\\n    ],\\n  })\\n);\\n\")), mdx(\"h3\", {\n    \"id\": \"building-the-server\"\n  }, \"Building the server\"), mdx(\"p\", null, \"Use a factory so the same server definition works with multiple transports:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\\nimport { z } from 'zod';\\n\\nconst todos = [\\n  { id: 1, title: 'Learn MCP basics', done: false },\\n  { id: 2, title: 'Ship demo server', done: false },\\n];\\nlet nextId = 3;\\n\\nexport function createMcpServer() {\\n  const server = new McpServer({ name: 'todo-mcp-server', version: '1.0.0' });\\n\\n  server.registerTool(\\n    'add_todo',\\n    {\\n      description: 'Add a new todo item',\\n      inputSchema: { title: z.string().min(1) },\\n    },\\n    async ({ title }) => {\\n      const todo = { id: nextId++, title, done: false };\\n      todos.push(todo);\\n      return { content: [{ type: 'text', text: JSON.stringify(todo, null, 2) }] };\\n    }\\n  );\\n\\n  server.registerTool('list_todos', { description: 'List all todo items' }, async () => ({\\n    content: [{ type: 'text', text: JSON.stringify(todos, null, 2) }],\\n  }));\\n\\n  server.registerTool(\\n    'mark_todo_done',\\n    {\\n      description: 'Mark a todo item as done by id',\\n      inputSchema: { id: z.number().int().positive() },\\n    },\\n    async ({ id }) => {\\n      const todo = todos.find((item) => item.id === id);\\n      if (!todo) {\\n        return { content: [{ type: 'text', text: `Todo ${id} not found` }], isError: true };\\n      }\\n      todo.done = true;\\n      return { content: [{ type: 'text', text: JSON.stringify(todo, null, 2) }] };\\n    }\\n  );\\n\\n  // register resource and prompt here (see snippets above)\\n\\n  return server;\\n}\\n\")), mdx(\"p\", null, \"Tool handlers return \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"{ content: [...] }\"), \". Set \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"isError: true\"), \" when a tool fails so the host can surface the error. Resource handlers return \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"{ contents: [...] }\"), \". Prompt handlers return \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"{ messages: [...] }\"), \".\"), mdx(\"p\", null, \"The demo uses an in-memory store so you can run it without API keys or a database.\"), mdx(\"h3\", {\n    \"id\": \"transports---stdio-vs-sse--streamable-http\"\n  }, \"Transports - stdio vs SSE / Streamable HTTP\"), mdx(\"p\", null, \"MCP separates \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"protocol\"), \" (JSON-RPC messages) from \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"transport\"), \" (how bytes move between client and server).\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Stdio (\", mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"StdioServerTransport\"), \")\")), mdx(\"p\", null, \"The client spawns your server as a child process. JSON-RPC goes over stdin/stdout.\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Use when:\"), \" Claude Desktop, Cursor, VS Code, Claude Code, local CLI agents\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Pros:\"), \" simplest setup, no ports or firewall rules, no OAuth\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Cons:\"), \" one client per process; cloud hosts cannot spawn your local binary\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\\nimport { createMcpServer } from './create-server.js';\\n\\nconst server = createMcpServer();\\nawait server.connect(new StdioServerTransport());\\n\")), mdx(\"p\", null, \"Write logs to \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"stderr\"), \" only - stdout is the protocol channel.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Remote HTTP - SSE (legacy) vs Streamable HTTP (current)\")), mdx(\"p\", null, \"Early MCP remote servers used \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"HTTP + SSE\"), \": POST for client\\u2192server requests, \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/sse-101/\"\n  }), \"Server-Sent Events\"), \" for server\\u2192client streaming. That transport is deprecated.\"), mdx(\"p\", null, \"New servers should use \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Streamable HTTP\"), \" (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"StreamableHTTPServerTransport\"), \"). It supports POST request/response, optional SSE for notifications, and session management. The v2 SDK removes server-side SSE entirely; client-side SSE remains for legacy servers.\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Scenario\"), mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Transport\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Claude Desktop, local dev\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"stdio\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Cursor / VS Code project MCP\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"stdio\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"ChatGPT Apps / Connectors\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Streamable HTTP over public HTTPS\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Legacy SSE-only clients\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"SSE client transport still exists; prefer Streamable HTTP for new servers\")))), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Streamable HTTP entry (stateless):\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';\\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\\nimport { createMcpServer } from './create-server.js';\\n\\nconst app = createMcpExpressApp();\\nconst PORT = Number(process.env.PORT) || 3000;\\n\\napp.post('/mcp', async (req, res) => {\\n  const server = createMcpServer();\\n  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });\\n\\n  await server.connect(transport);\\n  await transport.handleRequest(req, res, req.body);\\n\\n  res.on('close', () => {\\n    transport.close();\\n    server.close();\\n  });\\n});\\n\\napp.listen(PORT, () => {\\n  console.error(`MCP server listening on http://127.0.0.1:${PORT}/mcp`);\\n});\\n\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"createMcpExpressApp()\"), \" enables DNS rebinding protection when binding to localhost - recommended for local HTTP servers.\"), mdx(\"p\", null, \"Deploy Streamable HTTP behind HTTPS before exposing it to cloud clients. ChatGPT Connectors also require OAuth 2.1 for production use.\"), mdx(\"h3\", {\n    \"id\": \"connecting-mcp-clients\"\n  }, \"Connecting MCP clients\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Claude Desktop (stdio - primary demo path)\")), mdx(\"p\", null, \"Config file on Windows: \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"%APPDATA%\\\\Claude\\\\claude_desktop_config.json\"), \". Open it via \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Settings \\u2192 Developer \\u2192 Edit Config\"), \".\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-json\"\n  }), \"{\\n  \\\"mcpServers\\\": {\\n    \\\"todo-mcp\\\": {\\n      \\\"command\\\": \\\"node\\\",\\n      \\\"args\\\": [\\\"C:/path/to/demos/mcp-server-nodejs-demo/src/stdio.js\\\"]\\n    }\\n  }\\n}\\n\")), mdx(\"p\", null, \"Restart Claude Desktop after saving. Claude shows a tool approval UI before executing write operations.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Claude Desktop (remote HTTP)\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"claude_desktop_config.json\"), \" validates \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"stdio servers only\"), \" - do not put a bare \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"url\"), \" field there expecting it to work. For a public HTTPS MCP server, use \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Settings \\u2192 Connectors \\u2192 Add custom connector\"), \". For local HTTP during development, bridge with \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://www.npmjs.com/package/mcp-remote\"\n  }), mdx(\"inlineCode\", {\n    parentName: \"a\"\n  }, \"mcp-remote\")), \" as a stdio-launched proxy.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"ChatGPT (Connectors / Apps)\")), mdx(\"p\", null, \"ChatGPT has no local MCP config file. Register servers in \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Settings \\u2192 Apps\"), \" (or \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Connectors\"), \") with a name, description, and MCP server URL.\"), mdx(\"p\", null, \"Requirements:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Public HTTPS\"), \" endpoint (Streamable HTTP)\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"OAuth 2.1\"), \" for production connectors\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Stdio-only servers need an HTTP wrapper or tunnel before ChatGPT can reach them\")), mdx(\"p\", null, \"ChatGPT shows confirmation modals before write/modify tool calls.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Cursor\")), mdx(\"p\", null, \"Cursor uses \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".cursor/mcp.json\"), \" with the same stdio shape as Claude Desktop (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"command\"), \" + \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"args\"), \").\"), mdx(\"h3\", {\n    \"id\": \"what-else-matters\"\n  }, \"What else matters\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Security\"), \" - validate tool inputs, scope tools narrowly, and never expose secrets in resources.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Logging\"), \" - stderr only for stdio servers; avoid \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"console.log\"), \" on stdout.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Testing\"), \" - use \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"@modelcontextprotocol/sdk\"), \" client helpers with \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"StdioClientTransport\"), \" for smoke tests.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Spec evolution\"), \" - prefer Streamable HTTP over legacy SSE; watch the v2 SDK split when upgrading.\")), mdx(\"h3\", {\n    \"id\": \"demo\"\n  }, \"Demo\"), mdx(\"p\", null, \"Runnable scripts for this post live in the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"mcp-server-nodejs-demo\"), \" folder in the private demos repository. Get access via \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/demos\"\n  }), \"code demos\"), \".\"));\n}\n;\nMDXContent.isMDXComponent = true;"}},"pageContext":{"id":"d51e804e-6676-50e5-91db-6aacc23b59cb"}},"staticQueryHashes":["1961101537","2542493696"]}