Blog · 2026-08-31

From a local MCP server to a hosted connector

Port a stdio MCP server that only runs on your laptop into defineMcp, add connector true, deploy, and get a URL plus hosted OAuth.

To turn a local stdio MCP server into a connector your teammates can use, you re-express its tools, resources and prompts with defineMcp, add "connector": true to denkops.json, and deploy. You get a public HTTPS URL speaking streamable HTTP at /mcp, plus a hosted OAuth flow. Be clear-eyed about the one manual part: there is no automatic stdio-to-HTTP wrapper. You port your tool definitions by hand. It is usually an hour of work, and the payoff is that the server stops living only on your laptop.

Why can't I just point a client at my stdio server?

Because a stdio server has no address. The assistant starts your process, talks to it over stdin and stdout, and kills it. That is perfect for one developer and impossible for anyone else, no URL to paste, no login, no always-on process for the next call. A connector is the opposite: it runs in a process that stays up, answers at a stable URL, and lets one server serve every client. See what is an MCP connector for the full contrast.

Your tool *logic* carries over unchanged. What changes is how the tools are declared and how the transport is served. That declaration is what you port.

How do I port my tools into defineMcp?

Your stdio server has a list of tools, each with a name, a description, an input schema and a handler. defineMcp wants exactly the same four things, arranged as a config object you export as the module default. Resources and prompts map across the same way.

import { defineMcp } from "@denkopsai/sdk/mcp";
import { z } from "zod";
import { lookupContact, recentNotes } from "./crm";

export default defineMcp({
  name: "crm-tools",
  version: "1.0.0",
  tools: {
    find_contact: {
      description: "Find a contact by email address.",
      input: z.object({ email: z.string().email() }),
      handler: async ({ email }) => lookupContact(email),
    },
  },
  resources: {
    playbook: {
      uri: "file://playbook.md",
      description: "Team sales playbook.",
      mimeType: "text/markdown",
      read: () => ({ text: recentNotes(), mimeType: "text/markdown" }),
    },
  },
  prompts: {
    intro_email: {
      description: "Draft an intro email to a contact.",
      input: z.object({ name: z.string() }),
      build: ({ name }) => [
        { role: "user", content: `Write a short intro email to ${name}.` },
      ],
    },
  },
});

That is the whole surface. Tools take { description, input, handler }, where input is a Zod schema (recommended) or a plain JSON Schema. Resources take { uri, description, mimeType, read }. Prompts take { description, input, build }, and build returns an array of { role, content } messages. If you already build tools with Zod, mcp tools with zod and mcp resources and prompts go deeper on each.

The porting itself is mechanical: copy each tool's description and schema across, and call your existing handler from the new handler. Your business functions do not move. Only their declarations do.

What do I get after I deploy?

Add the config file, deploy, and the server that ran only for you now runs for anyone you allow.

{ "name": "crm-tools", "slug": "crm-tools", "runtime": "bun", "connector": true }

Deploy from Claude Code or the CLI (denkops deploy), and you get back a live URL like https://crm-tools.denkops.host/mcp and an API key. The "connector": true flag turns on a hosted OAuth stack: discovery metadata, dynamic client registration, and a login and consent screen, none of which you write. Your app code sees only authorized calls. The full flag walkthrough is in "connector: true" is a full OAuth stack.

Now a teammate pastes that one URL into their assistant, the client discovers OAuth and walks them through consent, and they are calling your tools. Same handlers, same schemas, but the server is not tied to your laptop anymore. To wire up the first client, see add your connector to Claude, and /deploy-mcp-server to start from scratch.

FAQ

Is there an automatic wrapper from a stdio MCP server to a hosted one?

No. You port your tool, resource and prompt definitions into `defineMcp` by hand. The handler logic carries over unchanged, so the work is mechanical, usually about an hour, but there is no tool that auto-converts a stdio server into a hosted HTTP one.

Do my tool handlers change when I move to defineMcp?

Not the logic. You wrap each existing function in a `defineMcp` tool entry with a description and an input schema, and call your function from the `handler`. Resources use `read`, prompts use `build`. The transport and OAuth are handled by the platform.

What do I get after deploying with connector true?

A public HTTPS URL serving streamable HTTP at `/mcp`, an API key, and a hosted OAuth flow with discovery, dynamic client registration and consent. Teammates or customers install it by pasting the URL, so the server no longer runs only on your machine.

Ship it yourself: bunx denkops deploy or say "deploy on DenkOps" from your coding agent.

Start on DenkOps →

← Debug a misbehaving tool call with logs and traces · An on-call ops connector for your assistant →