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.
Build a production container
Section titled “Build a production container”Build with npm
Section titled “Build with npm”Run npm install first. This creates the package-lock.json required by the
image build. Then run:
npm run container:buildThe 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.
Keep secrets out of the image
Section titled “Keep secrets out of the image”Provide secrets through your deployment platform at runtime. Do not add secrets to the image, Dockerfile, deployment configuration file, or source control.
Harden the running container
Section titled “Harden the running container”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
forwardedHopsset to the number of entries it appends - sets HTTPS and authority metadata
- forwards
Originonly when the client supplied it - leaves
Authorizationunchanged
Database examples are local-only
Section titled “Database examples are local-only”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.
Read example-specific notes
Section titled “Read example-specific notes”For example-specific runtime assets and dependency notes, use the maintained READMEs:
- Tool server
- API-backed server
- OpenAPI-backed server
- Resources and prompts server
- Progress streaming server
- HTML UI server
- React UI server
- Multi-instance PostgreSQL server
- Database-schema server
- MongoDB-backed server
- SOAP-backed server
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.
Answer questions about your data
Section titled “Answer questions about your data”Tool server. Read details from a small in-memory catalogue. Use it to learn the tool definition without another service to configure.
npm init @emseepea/tool-server -- my-serverConnect a public API
Section titled “Connect a public API”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.
npm init @emseepea/api-backed-server -- my-serverRead the API-backed server example.
Generate an API boundary from OpenAPI
Section titled “Generate an API boundary from OpenAPI”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.
npm init @emseepea/openapi-backed-server -- my-serverRead the OpenAPI-backed server example.
Add authentication or observability
Section titled “Add authentication or observability”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.
Add feedback to any starter
Section titled “Add feedback to any starter”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.
Share reference material and prompts
Section titled “Share reference material and prompts”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.
npm init @emseepea/resources-and-prompts-server -- my-serverRead the resources and prompts server example.
Report progress during a tool call
Section titled “Report progress during a tool call”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.
npm init @emseepea/progress-streaming-server -- my-serverRead the progress-streaming server example.
Add a web form or MCP App
Section titled “Add a web form or MCP App”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:
npm init @emseepea/html-ui-server -- my-serverFor React:
npm init @emseepea/react-ui-server -- my-serverRead 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.
Package an MCP App resource
Section titled “Package an MCP App resource”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],});Inputs and safety boundaries
Section titled “Inputs and safety boundaries”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.
Metadata and evidence
Section titled “Metadata and evidence”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:
npm init @emseepea/multi-instance-postgres-server -- my-serverRead 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.
npm init @emseepea/database-schema-server -- my-serverRead the database-schema server example.
Connect MongoDB collections
Section titled “Connect MongoDB collections”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.
npm init @emseepea/mongodb-backed-server -- my-serverRead the MongoDB-backed server example.
Connect a SOAP service
Section titled “Connect a SOAP service”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.
npm init @emseepea/soap-backed-server -- my-server