Odel

Overview

A thin wrapper over the official MCP TypeScript SDK, plus Odel's conventions.

The official Model Context Protocol TypeScript SDK owns the protocol. @odel/module-sdk adds only what Odel needs on top of it: the per-request context and secrets envelope, typed errors, input validators, response schemas, and config declared in code.

It is not a fork. Everything you know about registerTool, resources, and prompts works unchanged, because it is the official implementation — re-exported so you install one dependency instead of two.

Install

pnpm add @odel/module-sdk zod
pnpm add -D wrangler @cloudflare/workers-types

@modelcontextprotocol/sdk comes along as a dependency. zod is a peer dependency because you import it directly in your own schemas.

A module, end to end

import { createOdelServer, WebStandardStreamableHTTPServerTransport } from '@odel/module-sdk/server';
import { getModuleContext, validators } from '@odel/module-sdk';

function buildServer() {
	const server = createOdelServer({ name: 'my-module', version: '1.0.0' });

	server.registerTool(
		'greet',
		{ description: 'Greet the current user', inputSchema: { name: validators.nonEmptyString() } },
		async ({ name }, extra) => {
			const ctx = getModuleContext(extra);
			const result = { success: true as const, greeting: `Hello ${name}, from ${ctx.displayName}` };
			return { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result };
		}
	);

	return server;
}

export default {
	async fetch(request: Request): Promise<Response> {
		const server = buildServer();
		const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
		await server.connect(transport);
		return transport.handleRequest(request);
	}
};

Two things in there are deliberate rather than incidental.

A fresh server and transport per request. Isolates don't persist between calls in any way you should rely on, so building both inside fetch is the pattern that behaves the same on the first request and the ten-thousandth. With sessionIdGenerator: undefined the transport is stateless: it accepts a single tools/call with no initialize handshake first, which is exactly how Odel's proxy invokes a module.

createOdelServer rather than new McpServer. It is a three-line wrapper that registers one extra resource — see below. McpServer is re-exported if you need to construct it yourself.

The odel://config marker

Every server built with createOdelServer exposes an MCP resource at odel://config, shaped { secrets: [{ name, description, required }] } and derived from the config schema you declare (or { secrets: [] } if you declare none).

It does two jobs. It is how Odel tooling recognises an Odel module at all — the inspector tells an Odel server from a plain MCP server by whether this resource is there. And it is how a client discovers what a module needs, so a secret-entry form can be built from the module itself rather than from a list someone maintained by hand.

If you construct McpServer directly, call registerOdelConfig(server, configSchema) to expose it. Declaring config covers the schema side.

What Odel adds

What each import gives you

ImportProvides
@odel/module-sdk/servercreateOdelServer, McpServer, WebStandardStreamableHTTPServerTransport, and the CallToolResult / RequestHandlerExtra types
@odel/module-sdkgetModuleContext, getRequiredSecret, getOptionalSecret, createToolContext, parseConfig, configRequiredSecretNames, buildConfigManifest, registerOdelConfig, ODEL_CONFIG_URI, validators, ModuleError, ErrorCode, SuccessResponseSchema, SimpleSuccessSchema, and the types
@odel/module-sdk/odelThe same helpers as the root, as an explicit subpath

The root and /odel are interchangeable; the root is shorter and is what these pages use.

Where the code lives

On this page