@odel/module-sdk
v2.0.0
Published
SDK for building Odel modules — a thin, additive wrapper over the Model Context Protocol TypeScript SDK for Cloudflare Workers
Maintainers
Readme
@odel/module-sdk
A thin, additive wrapper over the Model Context Protocol TypeScript SDK for building Odel modules on Cloudflare Workers.
The official MCP SDK owns the protocol. This package adds only Odel's conventions on top: the per-request context/secrets envelope, typed errors, input validators, response schemas, and code-declared config. No fork, no hand-rolled protocol — just @modelcontextprotocol/sdk plus a handful of helpers.
Installation
npm install @odel/module-sdk zod
npm install -D wrangler @cloudflare/workers-types@modelcontextprotocol/sdk comes along as a dependency; zod is a peer dependency (you import it directly in your module).
Quick start
import { createOdelServer, WebStandardStreamableHTTPServerTransport } from '@odel/module-sdk/server';
import { getModuleContext, getRequiredSecret, validators } from '@odel/module-sdk';
import { z } from 'zod';
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); // { userId, displayName, conversationId?, requestId, timestamp }
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);
}
};Use createOdelServer rather than McpServer directly: it's a thin wrapper that always registers the odel://config marker resource (see below), so your module is identifiable as an Odel module and its declared config is discoverable — even if it declares none. (McpServer is still re-exported if you need it.)
A fresh server + transport per request is the canonical stateless pattern for Workers. With sessionIdGenerator: undefined the transport accepts a single tools/call with no initialize handshake — which is exactly how Odel's mcp-proxy invokes modules.
See examples/calculator-basic for a complete, runnable module.
The context envelope
Odel's mcp-proxy authenticates the user, resolves their per-module secrets, and injects them into each request's params._meta under namespaced keys:
app.odel/context— identity (userId,displayName,conversationId?,requestId,timestamp)app.odel/secrets— the per-user secret map for this module
The official SDK surfaces params._meta to tool handlers as extra._meta, so you read all of this from the handler's extra:
import { getModuleContext, getRequiredSecret, getOptionalSecret } from '@odel/module-sdk';
const ctx = getModuleContext(extra); // identity, with anonymous fallbacks
const apiKey = getRequiredSecret(extra, 'KEY'); // throws ModuleError if missing
const maybe = getOptionalSecret(extra, 'WEBHOOK'); // string | undefinedDeclaring config in code
Declare the secrets/config your module needs as a Zod schema. parseConfig validates the envelope and returns a typed, validated object; configRequiredSecretNames is what the dev-portal extracts into the module's required-secrets list.
import { parseConfig } from '@odel/module-sdk';
import { z } from 'zod';
export const configSchema = z.object({
RESEND_API_KEY: z.string().min(1).describe('Resend API key'),
FROM_ADDRESS: z.string().email().optional().describe('Override sender'),
});
// inside a handler:
const cfg = parseConfig(configSchema, extra); // { RESEND_API_KEY: string; FROM_ADDRESS?: string }The odel://config marker
Every server made with createOdelServer exposes an MCP resource at odel://config — shape { secrets: [{ name, description, required }] }, derived from your configSchema (or { secrets: [] } when you declare none). It does double duty: it's the marker that identifies an Odel module to Odel tooling (the inspector tells Odel servers from plain MCP servers by its presence), and it lets clients discover what config a module needs to drive secret-entry UI. If you construct McpServer yourself, call registerOdelConfig(server, configSchema?) to expose it.
What's exported
| Import | Provides |
| --- | --- |
| @odel/module-sdk/server | createOdelServer, McpServer, WebStandardStreamableHTTPServerTransport, CallToolResult, RequestHandlerExtra |
| @odel/module-sdk | getModuleContext, getRequiredSecret, getOptionalSecret, createToolContext, parseConfig, configRequiredSecretNames, buildConfigManifest, registerOdelConfig, ODEL_CONFIG_URI, validators, ModuleError, ErrorCode, SuccessResponseSchema, SimpleSuccessSchema, types |
| @odel/module-sdk/odel | same helper surface as the root (explicit subpath) |
License
MIT
