Skip to content

Choose an example

Each initializer creates a standalone app with private: true in package.json, application code, ordinary tests in test/, and language-model tests in eval/. Capability modules live in src/capabilities/ and are discovered once at startup. Replace my-server with an unused directory name. The first-server guide continues from the tool server starter.

Run npm install first. This creates the package-lock.json required by the image build. Then run:

Terminal window
npm run container:build

The Dockerfile uses npm ci and fails when the lockfile is absent.

Configure production behind a trusted proxy

Section titled “Configure production behind a trusted proxy”

The container is not safe for direct public exposure. Run it behind a trusted proxy and mount the required deployment configuration at runtime. Local npm start remains loopback-only on your computer. Container startup selects the fail-closed production-behind-proxy deployment profile and refuses to start the app unless EMSEEPEA_DEPLOYMENT_CONFIG_FILE points to a runtime-mounted JSON file.

The deployment file contains only non-secret policy:

{
"allowedAuthorities": ["mcp.example.com"],
"allowedOrigins": ["https://mcp.example.com"],
"trustedProxyAddresses": ["10.0.0.10"],
"forwardedHops": 0,
"rateLimit": {
"maxRequests": 100,
"windowMs": 60000,
"maxClients": 1000
}
}

If your platform gives you no fixed proxy address to list, have the proxy prove itself instead. Configure it to add a header carrying a secret, and replace trustedProxyAddresses with a proxyBoundary naming that header and the environment variable holding the secret:

{
"proxyBoundary": { "header": "x-proxy-token", "secretEnv": "EMSEEPEA_PROXY_SECRET" }
}

Set the header on your proxy before you deploy this. Until the proxy sends it, every request is refused. That is the safe direction for a mistake to go. The secret must be at least 32 characters.

The deployment file stays non-secret. It names the variable; your platform supplies the value at runtime, the same way you provide every other secret. In code the field is secret and carries the value itself; in this file it is secretEnv and names the variable.

The secret travels as a request header, and proxies and CDNs often log request headers by default. Turn off logging for that header at your proxy and anything in front of it.

Supply either trustedProxyAddresses or proxyBoundary, never both. A profile with neither is refused too. This replaces the address check and nothing else: the authority, origin, forwarded-protocol and rate-limit checks all still run.

How to prove a proxy with no fixed address explains the setup in full.

forwardedHops says how many entries the infrastructure in front appends to X-Forwarded-For after the client’s address. Leave it at 0 if your proxy replaces the header, which means it must carry exactly one entry. If your load balancer appends instead, set it to the number of entries it adds.

Set it from a header you have actually seen rather than from documentation. Log the raw X-Forwarded-For from a request you make yourself and count the entries after your own address.

Getting this number too high is the dangerous mistake. A caller can then put their own value in the position being read, and the rate limit never catches them. Nothing warns you.

Getting it too low is the safe mistake. Every caller ends up on one shared limit. That is wrong too, but you see it at once.

How to set forwardedHops explains both wrong values in full.

Those values show the shape only. Replace them with your real host, browser origin, trusted proxy address, and request limits.

Provide secrets through your deployment platform at runtime. Do not add secrets to the image, Dockerfile, deployment configuration file, or source control.

Run the image with:

  • a read-only root filesystem
  • a small writable temporary filesystem
  • all Linux capabilities dropped
  • no-new-privileges
  • the deployment file mounted read-only

Put the container behind a proxy that:

  • terminates TLS
  • sends the client address first in X-Forwarded-For, and appends nothing except its own entries
  • has forwardedHops set to the number of entries it appends
  • sets HTTPS and authority metadata
  • forwards Origin only when the client supplied it
  • leaves Authorization unchanged

Docker Compose starts the database for local development only. It is not a production deployment recipe. Use npm run db:start to start the local database. npm run db:reset deletes the local database volume and its data. You cannot undo this action.

For example-specific runtime assets and dependency notes, use the maintained READMEs:

For each starter, the linked GitHub example directory contains the public initializer package, README, changelog, and maintained example source. The commands use the default npm release channel.

Tool server. Read details from a small in-memory catalogue. Use it to learn the tool definition without another service to configure.

Terminal window
npm init @emseepea/tool-server -- my-server

Read the tool server example.

API-backed server. Call an external JSON API and return selected, checked fields as an MCP result. The example preserves useful service values instead of maintaining a second closed list that needs another release when the service adds a valid value.

Terminal window
npm init @emseepea/api-backed-server -- my-server

Read the API-backed server example.

OpenAPI-backed server. Generate TypeScript declarations and Zod runtime validators from a committed OpenAPI 3 contract. A local Swagger 2 fixture also demonstrates the conversion path. Choose the API-backed server above when no usable machine-readable contract exists.

Terminal window
npm init @emseepea/openapi-backed-server -- my-server

Read the OpenAPI-backed server example.

Every starter is open by default. Its app factory also accepts the same typed authentication and observability extensions. The starter’s README and tests show both open and protected composition, so choose a starter for the application shape instead of choosing a separate security template.

Discovery stays public by default. Protect it explicitly only when the catalogue itself is sensitive. Protected discovery shows each principal only the capabilities allowed by their permissions. OAuth metadata remains public so clients can learn how to authenticate.

Retire a capability through a marketplace review window

Section titled “Retire a capability through a marketplace review window”

For the visible, hidden-but-callable, and removal stages, follow Version a published MCP server. That guide also covers contract baselines, client checks, and marketplace update timing.

Feedback is a cross-cutting capability, not another application shape. Install @emseepea/feedback after choosing any starter. Add a public detailed submission, a protected durable support conversation, or both through the starter’s additionalTools option.

PostgreSQL and Firestore can keep append-only conversations and transactional notification outboxes. GitHub Issues and Zendesk can remain the authoritative support system, including native replies, actions, status, and notifications.

Choose a feedback mode and backend.

Provide readable resources, resource addresses with parameters, reusable prompts, and suggestions for prompt fields.

Use a static resource for content at one registered URI. Use a resource template for content at predictable URIs, such as invoice://{invoiceId}.

resources/list returns metadata for static resources registered with the server. resources/templates/list returns metadata for registered URI templates. By default, these methods do not query application records or expand templates into matching resource URIs.

To list the caller’s private records, add an authenticated, bounded resource inventory callback to a protected template. Listing returns metadata; use resources/read for contents.

For a large or searchable collection, provide a list or search tool that validates its inputs and limits the number of results. The tool can return useful record details and a concrete resource URI for each relevant result. A client can pass that URI to resources/read, which performs its own authorization check before reading the resource.

Terminal window
npm init @emseepea/resources-and-prompts-server -- my-server

Read the resources and prompts server example.

Public or protected tools can send updates over the same open POST response, then return one final result. In production, a trusted proxy can forward this response without buffering it. The framework authenticates and authorizes protected calls before application code or the event stream begins. This does not add saved sessions, replay, or subscriptions.

Terminal window
npm init @emseepea/progress-streaming-server -- my-server

Read the progress-streaming server example.

The native HTML and React examples show the same pea planting-plan form. Choose native HTML for fewer dependencies, or React to fit an existing React application. The HTML example keeps page, form, and stylesheet handlers in src/routes/. The React example also keeps its browser-script handler there. Both register the routes once during startup.

For native HTML:

Terminal window
npm init @emseepea/html-ui-server -- my-server

For React:

Terminal window
npm init @emseepea/react-ui-server -- my-server

Read the HTML UI server example or the React UI server example.

The React initializer also publishes the preview result as an MCP Apps resource. It links the tool with _meta.ui.resourceUri, returns a versioned ui:// resource using text/html;profile=mcp-app.

It listens for ui/notifications/tool-result after the MCP Apps initialization handshake, and declares empty _meta.ui.csp allowlists because the card loads no external content. The tool remains useful in clients that do not render UI.

The example also includes openai/outputTemplate and openai/widgetCSP as compatibility aliases for existing ChatGPT Apps integrations. New integrations should use the standard ui fields. Add only the domains the component actually connects to or loads resources from.

Use defineMcpAppResource when a tool opens a Model Context Protocol (MCP) App. Register the returned resource and pass the returned toolMetadata to that tool.

The maintained React UI server source and its packed initializer exercise this path.

bodyMarkup and styles are trusted, application-owned inputs. Do not interpolate untrusted input directly into these strings. Escape dynamic text before rendering it. The helper does not sanitize them or create landmarks, headings, accessible controls, themes, or host lifecycle behavior. Your app owns those parts.

The following example assumes ./mcp-app-client.js is a local bundle built before the server starts. The React starter builds this file for you.

import { createEmseepea, defineMcpAppResource, defineTool } from "@emseepea/server";
import { z } from "zod";
const app = defineMcpAppResource({
name: "pea-preview",
access: "public",
uri: "ui://pea/preview",
title: "Pea planting plan",
language: "en",
bodyMarkup: '<main id="app"><h1>Pea planting plan</h1></main>',
styles: "main { color: currentColor; }",
bundleUrl: new URL("./mcp-app-client.js", import.meta.url),
csp: { connectDomains: [], resourceDomains: [] },
prefersBorder: true,
});
const openingTool = defineTool({
name: "open-pea-preview",
access: "public",
description: "Open the pea planting plan preview.",
inputSchema: z.object({}),
_meta: app.toolMetadata,
handler: () => ({ text: "Pea planting plan available" }),
});
const server = createEmseepea({
name: "pea-preview-server", version: "1.0.0",
tools: [openingTool], resources: [app.resource],
});

The helper requires a canonical ui:// URI, a non-empty title, a valid language tag, and exactly one script string or local-file bundleUrl. It reads a local bundle once when the resource is defined, not on each read.

The optional mimeType defaults to text/html;profile=mcp-app. Use text/html+skybridge only when preserving an established compatibility contract. The helper uses the selected value for both the resource listing and returned content, and rejects every other MIME value.

It builds a UTF-8 HTML document with a viewport, escaped title and a validated, escaped language tag on <html lang>. It neutralizes </script inside an inline script. The app still owns its markup and CSS; use the script option only with trusted code.

The content security policy (csp) lists are connectDomains, resourceDomains, frameDomains, and baseUriDomains. Each entry must be an exact HTTPS origin, or an HTTP origin on localhost, 127.0.0.1, or [::1]. Paths, wildcards, duplicate origins, and other fields are rejected.

Each list may contain at most 32 origins, with at most 64 across all lists. Omitted lists default to empty lists. Declare only origins the app needs. An empty list allows no external origin in that category.

The resource listing and read use the selected MIME type and the same canonical URI. Resource metadata includes standard ui.csp and ui.prefersBorder plus the openai/widgetCSP and openai/widgetPrefersBorder compatibility aliases. toolMetadata carries both ui.resourceUri and openai/outputTemplate from that URI. Public and protected access rules remain those of ordinary resources.

Local source tests check the helper; packed initializer checks exercise the starter. Neither proves publication. Anonymous registry readback proves package publication; an exact deployed website revision proves this guide reached the website. None of those checks proves that an adopter’s production client rendered or used the app.

Share a report store between server instances

Section titled “Share a report store between server instances”

Interchangeable server instances use the same PostgreSQL store. Save a complete harvest report through one process and retrieve or update it through another, without exposing server identity or routing to the user. The garden bed and harvest date identify one report.

Use:

Terminal window
npm init @emseepea/multi-instance-postgres-server -- my-server

Read the multi-instance PostgreSQL server example.

Generate validation from a PostgreSQL schema

Section titled “Generate validation from a PostgreSQL schema”

Use an existing PostgreSQL schema as the internal contract. The initializer generates checked-in TypeScript and runtime validation, prefers reads and writes through a view, and includes one stored procedure as a secondary pattern.

Terminal window
npm init @emseepea/database-schema-server -- my-server

Read the database-schema server example.

Compare two collection policies in one project. Pea varieties use a MongoDB validator. Pea observations remain schemaless and are validated by the application before writes and after reads. Tools follow the user’s task rather than exposing either storage policy.

Terminal window
npm init @emseepea/mongodb-backed-server -- my-server

Read the MongoDB-backed server example.

Keep a local WSDL and XSD authoritative for a legacy SOAP service. Generate TypeScript from the contract, validate raw XML before parsing, and return small, described JSON to the model.

Terminal window
npm init @emseepea/soap-backed-server -- my-server

Read the SOAP-backed server example.