@us-all MCP Server Standard
May 15, 2026 · View on GitHub
Status: Living document. Reflects patterns proven across 6 production MCP servers as of 2026-05.
Audience: Authors of new
@us-all/*MCP servers, or contributors evolving existing ones.Why this exists: 2026 saw upstream vendors (Datadog, Google, Databricks, OpenMetadata 1.12, etc.) ship official MCP servers in most categories we operated in. The differentiation that survived was not "more tools" but "feature-rich + token-efficient by design" — concrete patterns that keep LLM context costs low while preserving deep coverage. This document captures those patterns so the next MCP server starts at the right place.
Use the toolkit
Most patterns below are pre-implemented in @us-all/mcp-toolkit. Install it instead of copying code:
pnpm add @us-all/mcp-toolkit
import {
ToolRegistry,
createSearchToolsMetaTool,
parseEnvList,
applyExtractFields,
extractFieldsDescription,
createWrapToolHandler,
wrapToolHandler,
} from "@us-all/mcp-toolkit";
This document explains the why behind each pattern; the toolkit provides the how.
Reference repositories
| repo | npm | size |
|---|---|---|
| openmetadata-mcp-server | @us-all/openmetadata-mcp | 156 tools |
| datadog-mcp-server | @us-all/datadog-mcp | 159 tools |
| google-drive-mcp-server | @us-all/google-drive-mcp | 96 tools |
| mlflow-mcp-server | @us-all/mlflow-mcp | 79 tools |
| unifi-mcp-server | @us-all/unifi-mcp | 52 tools |
| android-mcp-server | @us-all/android-mcp | 73 tools |
The four token-efficiency patterns
1. Tool registry + category-based ENV toggle
File: src/tool-registry.ts. Define a CATEGORIES const array that fits your domain (Datadog: metrics, monitors, logs, apm, rum, ...; OpenMetadata: core, governance, quality, ...). Wrap server.tool with a tool() helper that:
- Registers every tool in the registry (so
search-toolscan find them even if disabled). - Conditionally calls the underlying
server.toolonly if the current category is enabled per<PREFIX>_TOOLS(allowlist) /<PREFIX>_DISABLE(denylist) env vars.
In src/index.ts:
let currentCategory: Category = "default-cat";
function tool(name: string, description: string, schema: any, handler: any): void {
registry.register(name, description, currentCategory);
if (registry.isEnabled(currentCategory)) {
server.tool(name, description, schema, handler);
}
}
// Section header sets the category for everything below it
currentCategory = "metrics";
tool("query-metrics", "...", schema, handler);
Measured impact (real tools/list JSON sizes, ~4 chars/token):
| Server | Default | Narrow toggle | Reduction |
|---|---|---|---|
| openmetadata | 24K tokens | 4.6K (OM_TOOLS=search,core) | −81% |
| datadog | 25K tokens | 3.8K (DD_TOOLS=metrics,monitors) | −85% |
| google-drive | 18K tokens | 4.0K (GD_TOOLS=drive) | −78% |
| android | 9.2K tokens | 2.5K (ANDROID_TOOLS=device,ui) | −73% |
2. search-tools meta-tool (always enabled)
A search tool that queries the registry and returns matching tool names + descriptions. Always loaded regardless of category toggles, so even with a narrow allowlist users can discover what else exists. Re-launch with broader categories if needed.
export async function searchTools(params: { query: string; category?: string; limit?: number }) {
const matches = registry.search(params.query, params.category, params.limit);
return { query: params.query, matchCount: matches.length, summary: registry.summary(), matches };
}
3. extractFields response projection (auto-applied)
File: src/tools/extract-fields.ts. A pure helper that takes a comma-separated dotted-path expression with * wildcards and projects only the requested fields from a response.
extractFields="id,owner.name,columns.*.name,columns.*.dataType"
→ keeps only those fields, drops everything else
Wire it through createWrapToolHandler in the toolkit — the factory handles extractFields projection, MCP response shaping, and error sanitization for you:
import { createWrapToolHandler } from "@us-all/mcp-toolkit";
export const wrapToolHandler = createWrapToolHandler({
redactionPatterns: [/DD_API_KEY/i, /DD_APP_KEY/i], // merged with toolkit defaults
errorExtractors: [
{
match: (e) => e instanceof WriteBlockedError,
extract: (e) => ({ kind: "passthrough", text: (e as Error).message }),
},
{
match: (e) => e instanceof DatadogApiError,
extract: (e) => ({
kind: "structured",
data: { message: (e as Error).message, status: (e as DatadogApiError).code },
}),
},
],
});
For a zero-config wrapper, import the prebuilt wrapToolHandler from the toolkit instead. Default redaction covers api_key, app_key, authorization, bearer …, password, secret, token.
Then declare the field on read tool schemas you want LLMs to use it on:
const ef = z.string().optional().describe(extractFieldsDescription);
export const getTableSchema = z.object({
id: z.string(),
// ...
extractFields: ef,
});
⚠ Schemas must opt in. Even with auto-apply wired in wrapToolHandler, the MCP SDK validates input against each tool's zod schema and drops unknown fields before the handler sees them. So params.extractFields is undefined unless the tool's schema declares it. Either add the field per tool, or use .passthrough() if you want it implicit:
export const listHostsSchema = z.object({
count: z.coerce.number().optional(),
// ...
}).passthrough(); // ← extractFields will pass through to wrapToolHandler
Real-world impact (live measurement, datadog v1.11.1):
| Tool | Default | With extractFields | Reduction |
|---|---|---|---|
get-monitors (5 monitors) | 594 tokens | 148 tokens | −75% |
get-monitors (20 monitors) | 3,108 tokens | 622 tokens | −80% |
list-hosts (10 hosts, schema not declared) | 3,965 tokens | 3,965 tokens | 0% (SDK drops unknown field) |
4. MCP Resources for hot entities
File: src/resources.ts. Use the SDK's server.registerResource(name, ResourceTemplate, metadata, callback) to expose hot entities by URI. Resources are application-driven (host UI picks them) and don't consume tool schema tokens until read.
server.registerResource(
"table",
new ResourceTemplate("om://table/{fqn}", { list: undefined }),
{ title: "OpenMetadata Table", mimeType: "application/json" },
async (uri, vars) => {
const data = await omClient.get(`/tables/name/${encodeURIComponent(String(vars.fqn))}`, {
fields: "columns,owners,tags",
});
return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify(data) }] };
},
);
Use a custom URI scheme matching your domain (om://, mlflow://, dd://, etc.).
Aggregation tools (round-trip elimination)
When LLMs reliably issue 3+ tool calls in sequence to assemble a "summary view," provide a single aggregation tool that does it server-side with Promise.allSettled for partial-failure tolerance.
// openmetadata: get-table-summary
// = get-table-by-name + get-lineage-by-name + (opt) get-table-sample-data + (opt) list-test-cases
// mlflow: summarize-run
// = get-run + (opt) get-metric-history-per-key + (opt) list-artifacts
Naming: <get|analyze|summarize>-<entity>-<view>. Always include a summary object in the response with metadata (counts, what was/wasn't fetched).
Project layout
src/
├── index.ts # entry point: server setup + tool() helper + currentCategory + registrations
├── config.ts # env vars incl. enabledCategories / disabledCategories parsing
├── client.ts # HTTP client wrapper for the upstream API
├── tool-registry.ts # CATEGORIES const + ToolRegistry class + searchTools meta-tool
├── resources.ts # MCP Resources via registerResource (hot entity URIs + Apps SDK ui:// templates)
├── ui/ # Apps SDK card HTML templates (when shipped) — copied to dist/ui/ at build
└── tools/
├── utils.ts # wrapToolHandler built via createWrapToolHandler factory + domain extractors, assertWriteAllowed, custom error classes
├── extract-fields.ts # applyExtractFields helper + extractFieldsDescription
├── aggregations.ts # round-trip-elimination tools (get-X-summary)
└── <category>.ts # one file per logical category, exporting Schema + handler pairs
If you ship Apps SDK cards (see Apps SDK Cards below), add this to the build script so HTML templates land alongside compiled JS:
"build": "tsc && node -e \"require('fs').cpSync('src/ui','dist/ui',{recursive:true})\""
Runtime & transport
Use startMcpServer from @us-all/mcp-toolkit/runtime (v1.2.0+) instead of hand-rolling stdio bootstrap. One line replaces 12, and the same code transparently supports Streamable HTTP for ChatGPT Apps SDK / remote clients.
import { startMcpServer } from "@us-all/mcp-toolkit/runtime";
startMcpServer(server).catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Transport is selected by env (default stdio — no breaking change for existing users):
| Env var | Default | Description |
|---|---|---|
MCP_TRANSPORT | stdio | http to enable Streamable HTTP |
MCP_HTTP_TOKEN | — | Bearer token. Required when MCP_TRANSPORT=http (unless MCP_HTTP_SKIP_AUTH=true) |
MCP_HTTP_PORT | 3000 | HTTP listen port |
MCP_HTTP_HOST | 127.0.0.1 | Bind host. Localhost binds auto-enable DNS rebinding protection. |
MCP_HTTP_SKIP_AUTH | false | Skip Bearer auth — only when an upstream proxy already authenticates |
HTTP mode exposes POST/GET/DELETE /mcp (Bearer-auth JSON-RPC) and GET /health (public liveness check).
If your index.ts does pre-startup work (e.g. capability detection in google-drive-mcp), keep main() and replace only the transport call:
async function main() {
validateConfig();
await detectCapabilities(); // pre-flight
await startMcpServer(server); // transport
}
main().catch(...);
Known limitation (2026-05): stateless HTTP mode (the toolkit default) handles initialize within a single request fine, but separate follow-up HTTP requests don't share session state — fine for short-lived ChatGPT calls, may need stateful mode for long-running custom clients. Tracked for a future minor.
Apps SDK cards
When a tool has structured output that benefits from visual rendering (an SLO snapshot, a comparison table, a permission audit), ship it as a ChatGPT Apps SDK card. Claude clients ignore the metadata and use the existing JSON text — non-breaking.
1. Author the HTML template at src/ui/<card>.html. The template runs in a sandboxed iframe with access to window.openai.toolOutput (your tool's structuredContent):
<script>
function update() {
document.getElementById('app').innerHTML = render(window.openai && window.openai.toolOutput);
}
update();
window.addEventListener('openai:set_globals', update); // re-render on tool re-call
</script>
Keep templates self-contained (inline CSS, vanilla JS, dark/light via prefers-color-scheme). No external CDNs, no module imports. Sandbox limits apply.
2. Register the template as an MCP resource with mime type text/html+skybridge (OpenAI's accepted value as of 2026-05) plus both _meta aliases for cross-vendor portability:
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const UI_DIR = join(dirname(fileURLToPath(import.meta.url)), "ui");
const CARD_HTML = readFileSync(join(UI_DIR, "my-card.html"), "utf-8");
server.registerResource(
"my-card",
"ui://widget/my-card.html",
{
title: "My card",
mimeType: "text/html+skybridge",
_meta: {
"openai/outputTemplate": "ui://widget/my-card.html",
"ui.resourceUri": "ui://widget/my-card.html",
},
},
async (uri) => ({
contents: [{
uri: uri.toString(),
mimeType: "text/html+skybridge",
text: CARD_HTML,
}],
}),
);
3. Wrap the target tool so its result includes structuredContent + _meta["openai/outputTemplate"]. Reuse wrapToolHandler for redaction/error handling and post-process the success path:
const CARD_URI = "ui://widget/my-card.html";
const wrappedHandler = wrapToolHandler(myToolFn);
async function myToolWithCard(args: Parameters<typeof wrappedHandler>[0]) {
const result = await wrappedHandler(args);
if (result.isError) return result;
try {
const structured = JSON.parse(result.content[0].text);
return {
...result,
structuredContent: structured,
_meta: {
"openai/outputTemplate": CARD_URI,
"ui.resourceUri": CARD_URI,
},
};
} catch { return result; }
}
tool("my-tool", "...", mySchema.shape, myToolWithCard);
Conventions:
- One render-only tool per card. Don't decorate every tool with
outputTemplate— only the one you want surfaced visually. - URI scheme
ui://widget/<card>.html(OpenAI's documented namespace; works alongside server-specific schemes likedd://,mlflow://). - Card name in the tool description (e.g. "Renders an Apps SDK card on ChatGPT clients") so the model knows it's available.
- Verify with Playwright + mock data before shipping; visual rendering only happens in ChatGPT.
Other conventions
- Read-only by default: gate all create/update/delete behind
<PREFIX>_ALLOW_WRITE=true. - Sensitive token redaction: handled by
createWrapToolHandlerdefaults (api/app key, authorization, bearer, password, secret, token). Pass domain-specific patterns (literal env var names likeDD_API_KEY,OPENMETADATA_TOKEN,X-API-KEY) via theredactionPatternsoption. - Schema-first: every tool exports
<name>Schema(zod) +<name>handler. Every field has.describe(). - Categories cover everything: include even infrequent tools (events, audit) so users can disable them via
<PREFIX>_DISABLErather than fork. packageManager: "pnpm@10.30.2"+pnpm.overridesfor transitive vulnerability pinning.- CI: Node 24 in publish workflow (Node 22 has the
npm install -g npm@latestMODULE_NOT_FOUND issue withpromise-retry). Trusted publishing requires npm >= 11.5.1 (bundled in Node 24). - Versioning: SemVer. New schema field on existing tool → minor. New tool/category → minor. Pure refactor or transitive dep pin → patch.
When to deviate
- Tiny tool surface (<20 tools): skip the registry/category complexity. unifi-mcp ran fine without categories at v1.0; only added them when the connector tools brought it past 50.
- Highly heterogeneous responses:
extractFieldsworks best on consistent JSON. For binary or streamed responses (e.g. Android screenshot), don't wire it through — let those handlers returnwrapImageToolHandlerdirectly. - Single-tenant / restricted use case: ENV toggles add value mainly for shared MCP installations where users have varied workflows. For a single-team internal tool, you can skip them.
Maintenance
- Run
node scripts/measure-tokens.mjs(when added by E-6) on every PR; flag ifdefaultschema tokens grow more than 20%. - Keep this doc updated as new patterns prove out across multiple repos.
v2 Migration Notes (@modelcontextprotocol/sdk 1.x → 2.x)
Status (2026-05-15): SDK 2.x is alpha.2. v1.x remains the recommended pin. PoC verified the migration path on mcp-toolkit#migrate/sdk-2-alpha (7feb5e1, 54/54 tests) and datadog-mcp-server#migrate/sdk-2-alpha (fe4d6be, 23/23 tests). Apply when stable v2 ships.
Package layout change
| v1 | v2 |
|---|---|
@modelcontextprotocol/sdk | @modelcontextprotocol/server (McpServer, ResourceTemplate, StdioServerTransport, types) |
| (same) | @modelcontextprotocol/node (NodeStreamableHTTPServerTransport) |
| (n/a) | @cfworker/json-schema — declared optional peer but must be installed until #2093 is fixed |
Consumer migration (≈25 lines per repo)
Key insight: change the local tool() helper once and the hundreds of registration call sites stay unchanged. Wrap ZodRawShape inside the helper.
// before (v1)
function tool(name, description, schema: any, handler: any) {
if (registry.isEnabled(currentCategory)) {
server.tool(name, description, schema, handler);
}
}
// after (v2)
import { z, type ZodRawShape } from "zod";
function tool(name, description, shape: ZodRawShape, handler: any) {
if (registry.isEnabled(currentCategory)) {
server.registerTool(
name,
{ description, inputSchema: z.object(shape) },
handler,
);
}
}
Other touch points
- All
McpServer/ResourceTemplateimports:@modelcontextprotocol/sdk/server/mcp.js→@modelcontextprotocol/server StdioServerTransport: also exports from@modelcontextprotocol/server(no/server/stdiosubpath despite what the upstream migration guide says — it's wrong)StreamableHTTPServerTransport→NodeStreamableHTTPServerTransportfrom@modelcontextprotocol/noderegisterPromptargsSchema: must be a Standard Schema object (z.object({...})), not a raw shape- Test mocks:
vi.mock("@modelcontextprotocol/sdk/server/mcp.js", …)→vi.mock("@modelcontextprotocol/server", …). The mock's.tool()method becomes.registerTool(name, config, handler)wheredescriptionlives onconfig.description.
Known v2-alpha gotchas
@cfworker/json-schemahard-import — see upstream #2093. Add it as a direct dep until fixed./server/stdiosubpath does not exist — migration guide is incorrect. ImportStdioServerTransportfrom@modelcontextprotocol/servermain.- Node 18 dropped — minimum is Node 20 (we're on Node 22, no impact).
- CommonJS dropped — ESM only (we're already ESM, no impact).