OMP Docs
Extend

Authoring MCP servers

Build a standards-based tool server, connect it to omp, and verify the handshake and tool calls end to end.

Build once, use it from many clients

An MCP server turns an API, local service, or workflow into tools that omp can discover and use. Choose MCP when the integration should also work with other MCP clients. For an omp-only integration, a custom tool is usually smaller.

The quickest path is a local stdio server. The official SDK handles JSON-RPC framing, version negotiation, and the initialization sequence, so your code can focus on tool schemas and behavior.

Build a minimal stdio server

Install dependencies

Install the current TypeScript server SDK and Zod in your project:

npm install @modelcontextprotocol/server zod

Implement the server code

Save this as mcp/hello-server.mjs:

import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

const server = new McpServer({
  name: "hello",
  version: "1.0.0",
});

server.registerTool(
  "greet",
  {
    description: "Greet one person by name.",
    inputSchema: z.object({
      name: z.string().min(1).describe("The person's name"),
    }),
  },
  async ({ name }) => ({
    content: [{ type: "text", text: `Hello, ${name}!` }],
  }),
);

await server.connect(new StdioServerTransport());

Do not write logs to stdout

Do not print logs to stdout in a stdio server. stdout carries newline-delimited MCP messages; a single debug line can corrupt the framing. Write diagnostics to stderr instead.

Connect it to omp

Add a project config at .omp/mcp.json:

{
  "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
  "mcpServers": {
    "hello": {
      "type": "stdio",
      "command": "node",
      "args": ["./mcp/hello-server.mjs"]
    }
  }
}

Reload and test

Start omp in that project, then run:

/mcp reload
/mcp test hello

/mcp test hello creates a temporary connection, completes initialization, calls tools/list, and reports the server identity and discovered tool names. It does not call a tool. Exercise the handler with a normal prompt:

Use the hello server to greet Ada.

You should see Hello, Ada!. The child process starts in the omp project directory unless the server config sets cwd, so relative args above resolve from the project root.

Choose a transport

Best for a local server launched for one omp session.

  • Contract: Read JSON-RPC from stdin, write newline-delimited JSON-RPC to stdout, keep diagnostics on stderr.
  • Config: Omit type or use "type": "stdio"; set command, with optional args, env, and cwd.

stdio is the default when type is absent. A remote entry without "type": "http" is therefore treated as stdio and fails because it has no command.

Publish the same tool over Streamable HTTP

The current SDK can serve both current and 2025-era clients. For a small Node-hosted endpoint, install its Node adapter:

npm install @modelcontextprotocol/node

A minimal loopback server looks like this:

import { createServer } from "node:http";
import {
  localhostHostValidation,
  localhostOriginValidation,
  toNodeHandler,
} from "@modelcontextprotocol/node";
import { createMcpHandler, McpServer } from "@modelcontextprotocol/server";
import * as z from "zod/v4";

const handler = createMcpHandler(() => {
  const server = new McpServer({ name: "hello-http", version: "1.0.0" });
  server.registerTool(
    "greet",
    {
      description: "Greet one person by name.",
      inputSchema: z.object({ name: z.string().min(1) }),
    },
    async ({ name }) => ({
      content: [{ type: "text", text: `Hello, ${name}!` }],
    }),
  );
  return server;
});

const nodeHandler = toNodeHandler(handler);
const validateHost = localhostHostValidation();
const validateOrigin = localhostOriginValidation();

createServer((request, response) => {
  if (!validateHost(request, response) || !validateOrigin(request, response)) return;
  void nodeHandler(request, response);
}).listen(3000, "127.0.0.1");

Configure the running endpoint:

{
  "mcpServers": {
    "hello-http": {
      "type": "http",
      "url": "http://127.0.0.1:3000/mcp"
    }
  }
}

Public deployment security

For a public deployment, validate Host and Origin, authenticate before dispatching to the MCP handler, enforce TLS, and pass only verified identity to tool code. The handler does not authenticate requests for you.

Public protocol contract

Using an MCP SDK is strongly recommended. If you implement the wire protocol yourself, these are the interoperability points omp relies on:

PhaseWhat the server must do
initializeAccept a JSON-RPC 2.0 request. omp currently offers MCP protocol revision 2025-11-25. Reply with the negotiated protocolVersion, serverInfo, and capabilities.
notifications/initializedAccept the notification before expecting later session traffic. Notifications have no request ID and receive no response.
tools/listWhen capabilities.tools is declared, return { "tools": [...] }. If the result has nextCursor, omp follows it until all pages are loaded.
tools/callAccept { "name": string, "arguments"?: object } and return a tool result. Echo each JSON-RPC request ID exactly; valid IDs may be numbers or strings.
Server requestsomp answers ping and roots/list. roots/list returns the active project as a file: URI. Do not assume a client supports other server-to-client methods unless it advertised them.
Tool changesIf tools can change, advertise tools.listChanged and send notifications/tools/list_changed after initialization. omp then reloads that server's tool list without a session restart.

For Streamable HTTP, preserve Mcp-Session-Id when you issue one and accept MCP-Protocol-Version: 2025-11-25 on requests after initialization. Support the HTTP methods, status codes, media types, and optional SSE channel defined by the public MCP transport specification. The official SDK handles these details.

omp sends numeric JSON-RPC request IDs by default. A server should accept both JSON-RPC-permitted forms; requiring one representation reduces compatibility with other clients.

Define tools that travel well

A listed tool needs a stable name, a concrete description, and an inputSchema whose top level is a JSON Schema object. With the SDK above, a Standard Schema library such as Zod produces that schema for you.

Good schemas help the client select the tool and construct valid arguments:

  • Describe the user-visible action and when it is appropriate.
  • Use explicit property descriptions, bounds, enums, and required fields.
  • Keep optional fields genuinely optional; do not require clients to send empty placeholders.
  • Validate permissions and inputs again in the handler. A schema is guidance and validation, not authorization.
  • Keep names stable once users depend on them.

omp namespaces discovered tools by server, lowercases their names, replaces characters outside [a-z_] with underscores, and caps the mounted name at 64 characters with a stable hash suffix when needed. Distinct raw names that normalize identically collide; omp keeps one deterministic winner. Prefer short, distinct lowercase names such as search_issues or create_invoice. Users ask for the capability in natural language rather than typing the mounted name.

Return results and errors

Every successful call should return content. Text is the most portable content type:

return {
  content: [{ type: "text", text: "Created invoice INV-1042." }],
};

omp also accepts MCP image content (data as base64 plus mimeType) and embedded resource content. If you also return structuredContent, include a useful text representation in content; do not make the human-readable result depend on structured output support.

Use isError: true for an expected, user-correctable failure. This keeps the failure attached to the tool call and gives the model information it can act on:

return {
  content: [{ type: "text", text: "Customer C-17 does not exist." }],
  isError: true,
};

Do not leak secrets in error traces

Reserve thrown exceptions or JSON-RPC error responses for protocol violations, unavailable dependencies, and unexpected server faults. omp displays expected isError results as tool errors; transport and JSON-RPC failures surface as MCP errors. Never include secrets, tokens, or private stack traces in either form.

For protected HTTP tools, a standards-based authorization challenge may be returned with an error in _meta["mcp/www_authenticate"]. omp recognizes that public MCP challenge and can reauthorize before retrying when the server is configured for OAuth.

Debugging loop

Test the server independently

For the stdio example, run node --check mcp/hello-server.mjs, then use the official inspector:

npx @modelcontextprotocol/inspector node ./mcp/hello-server.mjs

Confirm that greet appears and call it with { "name": "Ada" }.

Reload discovery

After editing .omp/mcp.json, run /mcp reload, then /mcp list to confirm the server name and source file.

Test discovery and schemas

Run /mcp test hello. It verifies initialization and tools/list, but not the handler itself.

Test the real user path

Ask omp to perform the action in natural conversation, then inspect the returned content. For dynamic servers, /mcp notifications helps confirm list-change notifications.

Restart only what changed

Use /mcp reconnect hello after changing local server code. Use /mcp reload after changing discovery or config.

Troubleshooting symptoms

Ship checklist

  • The server negotiates 2025-11-25 with omp and does not send session traffic before initialization completes.
  • stdio stdout contains protocol frames only; hosted HTTP has TLS, authentication, and Host/Origin validation.
  • tools/list returns stable names, descriptions, object input schemas, and every pagination cursor correctly.
  • Every tools/call path returns useful content; expected failures return isError: true.
  • The Inspector can list and call each tool, and /mcp test <name> reports the same inventory.
  • A natural-language request in omp exercises the real handler successfully.

Source: omp.sh — MIT, Stencil Labs. Initially imported on 2026-10-03; updates via automatic sync.

OMP Docs· always in sync with upstream
oh-my-piomp.sh/docsContent MIT · Stencil Labs