Typescript MCP Server Generator
github/awesome-copilot
Generate a complete MCP server project in TypeScript using the MCP TypeScript SDK v2 (@modelcontextprotocol/server) with tools, resources, and proper configuration
Reference for core and server configuration in @cyanheads/mcp-ts-core.
$ npx skills add cyanheads/pubmed-mcp-server --skill api-config -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install cyanheads/pubmed-mcp-server api-config --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .claude/skills && cp -r skills-src/framework-skills/api-config .claude/skills/api-config && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "api-config" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-config into .claude/skills/api-config/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-config", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-configType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add cyanheads/pubmed-mcp-server --skill api-config -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install cyanheads/pubmed-mcp-server api-config --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .agents/skills && cp -r skills-src/framework-skills/api-config .agents/skills/api-config && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "api-config" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-config into .agents/skills/api-config/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-config", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add cyanheads/pubmed-mcp-server --skill api-config -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install cyanheads/pubmed-mcp-server api-config --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/framework-skills/api-config .cursor/skills/api-config && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "api-config" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-config into .cursor/skills/api-config/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-config", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/cyanheads/pubmed-mcp-server.git --path framework-skills/api-config--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add cyanheads/pubmed-mcp-server --skill api-config -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install cyanheads/pubmed-mcp-server api-config --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/framework-skills/api-config .gemini/skills/api-config && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "api-config" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-config into .gemini/skills/api-config/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-config", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install cyanheads/pubmed-mcp-server api-configInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add cyanheads/pubmed-mcp-server --skill api-config -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .github/skills && cp -r skills-src/framework-skills/api-config .github/skills/api-config && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "api-config" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-config into .github/skills/api-config/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-config", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add cyanheads/pubmed-mcp-server --skill api-config -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install cyanheads/pubmed-mcp-server api-config --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/framework-skills/api-config .opencode/skills/api-config && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "api-config" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-config into .opencode/skills/api-config/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-config", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
api-configReference for core and server configuration in @cyanheads/mcp-ts-core.
API Config is an agent skill from cyanheads/pubmed-mcp-server. Reference for core and server configuration in @cyanheads/mcp-ts-core. Covers env var tables with defaults, priority order, server-specific Zod schema pattern, and Workers lazy-parsing requirement.
Its SKILL.md is about 6.7k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Research & Science, covering MCP servers. It works with Zod, Model Context Protocol and npm. The repository describes itself as: Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms via MCP. STDIO or Streamable HTTP. The licence is Apache-2.0.
4 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 5a417fb. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are typescript).
From the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
MCP_AUTH_SECRET_KEYMCP_REQUEST_STATE_KEYSUPABASE_SERVICE_ROLE_KEYSUPABASE_ANON_KEYOPENROUTER_API_KEYSPEECH_TTS_API_KEYSPEECH_STT_API_KEYFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
API Config loads about 6.7k tokens when it runs. Until then it costs about 53 tokens; SKILL.md has 2,831 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
th `Invalid option`. What makes a blank `.env` line take the default is the normalization layer described under **UnsetAutomated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from cyanheads/pubmed-mcp-server at commit 5a417fb, republished under its Apache-2.0 licence (© cyanheads). 2,831 words, ~6,693 tokens.
.claude/skills/api-config/SKILL.md (or your agent's skills folder).Configuration has two layers: core config (managed by the framework, env-driven) and server config (your own Zod schema for domain-specific env vars). Never merge them.
Import: AppConfig, config, parseConfig, resetConfig, ConfigSchema from @cyanheads/mcp-ts-core/config.
Managed by @cyanheads/mcp-ts-core. Validated via Zod from environment variables. Uses a lazy proxy — parsing is deferred until the first property read.
Priority (highest to lowest):
name/version/title/websiteUrl/description/icons options passed to createApp() or createWorkerHandler()sessionMode.default passed to createApp() — a default, so it sits below the env var it seeds, unlike the identity options abovepackage.json fieldsWhere package.json is read from: the application root — the nearest package.json at or above the process entry module (process.argv[1]), which is the served package on every launch path (npx, .mcpb, a client config naming dist/index.js), none of which run from the package root. The launching client's working directory is never the anchor: a stdio client starts the server from wherever it happens to be, so reading identity from there makes a server report a foreign project's name and version. When the entry module is a tool installed under a node_modules tree and the process runs from the directory owning that tree — a test runner is the usual case — the nearest manifest at or above the working directory wins instead. That also covers a workspace monorepo, where the runner is hoisted to the repo root while the process runs from a package directory: an owner that is a strict ancestor of the working directory qualifies only when it declares a workspace (a workspaces field in its manifest, or a pnpm-workspace.yaml beside it), which is what keeps a cache prefix or a plain project root — equally ancestors of a working directory inside them — from overriding an installed package's own identity. The owner is the outermost node_modules boundary, so a transitively-installed runner and a pnpm isolated layout resolve the same way. With no manifest reachable, the framework's own identity is the fallback.
| Env Var | AppConfig field | Default | Notes |
|---|---|---|---|
MCP_SERVER_NAME | mcpServerName | package.json name | Overrides package name |
MCP_SERVER_VERSION | mcpServerVersion | package.json version | Overrides package version |
MCP_SERVER_DESCRIPTION | mcpServerDescription | package.json description | Optional; createApp({ description }) wins when set |
PACKAGE_NAME | pkg.name | package.json name | Rarely needed |
PACKAGE_VERSION | pkg.version | package.json version | Rarely needed |
SDK identity fields (API-only, no env var equivalent — passed to createApp() / createWorkerHandler(), forwarded to initialize and /.well-known/mcp.json):
| Option | Type | Notes |
|---|---|---|
title | string? | Human-readable display name shown in client listings |
websiteUrl | string? | Canonical homepage / repository URL |
description | string? | One-line description; wins over MCP_SERVER_DESCRIPTION when set |
icons | Implementation['icons']? | Array of icon objects: { src, mimeType?, sizes?: string[], theme?: 'light'|'dark' } |
cacheHints | CacheHints? | Cache hints for the 2026-07-28 cacheable results, keyed by operation — see below |
sessionMode | SessionMode | { default?: SessionMode; require?: 'stateful' } | Session posture declared in code — see below |
cacheHints)API-only, no env var. Sets the ttlMs / cacheScope a client may cache a cacheable result for on protocol revision 2026-07-28. Keys are the closed set of cacheable operations: tools/list, prompts/list, resources/list, resources/templates/list, resources/read, server/discover.
await createApp({
cacheHints: {
'tools/list': { ttlMs: 3_600_000, cacheScope: 'public' },
'resources/read': { ttlMs: 60_000 },
},
});ttlMs — cache lifetime in milliseconds; must be a non-negative safe integer. An invalid value fails at startup with a ConfigurationError naming the field.cacheScope — 'private' (only the requesting client may cache) or 'public' (shared caches may too).cacheHint overrides the resources/read entry for that resource, field by field — see the add-resource skill.ttlMs: 0, cacheScope: 'private'). Responses to 2025-era clients are never affected.sessionMode)Declares the session posture in src/ instead of leaving it to a deployment's MCP_SESSION_MODE. The bare string is shorthand for { default }. HTTP only — MCP_SESSION_MODE has no effect on stdio.
await createApp({ sessionMode: 'stateless' }); // default only
await createApp({ sessionMode: { default: 'stateful', require: 'stateful' } }); // and enforceddefault applies only when MCP_SESSION_MODE carries no meaningful value. An empty string and a whole-value unsubstituted ${…} placeholder both read as unset on the config path, so both fall through to the option rather than to the schema default (auto). An explicit MCP_SESSION_MODE always wins.require: 'stateful' fails startup with a ConfigurationError naming the conflicting env value when the resolved HTTP mode is stateless, before any service is constructed. Declare it when a handler gates a destructive action behind ctx.requestInput / inputRequired.elicit — see the MCP_SESSION_MODE row for why that combination is unusable for 2025-era clients. There is no require: 'stateless'; nothing needs statelessness to work.transport.sessionMode follows automatically — resolveSessionMode is the single resolution the manifest, the session store, and the ctx.sessionId gate all read — and still never publishes auto.MCP_SESSION_MODE is not in CORE_ENV_BINDINGS, so a [vars] entry reaches process.env only through extraEnvBindings.| Env Var | AppConfig field | Default | Notes |
|---|---|---|---|
NODE_ENV | environment | development | Aliases: dev→development, prod→production, test→testing |
MCP_LOG_LEVEL | logLevel | debug | The floor for every log sink — stderr, the files, OTLP export, and the ctx.log mirror to the client (notifications/message), which a client's own level can only narrow. Compared on the RFC 5424 order, so notice drops info and crit drops error. Aliases: warn→warning, err→error, fatal/silent→emerg, trace→debug, information→info |
LOGS_DIR | logsPath | <app-root>/logs | Node.js only; absolute paths are used verbatim, relative ones resolve against the application root (see Core config) — never the framework's install directory. A file under it that cannot be opened (read-only mount, another user's directory) is dropped at startup with one warning naming it and the error code; stderr and the other files keep logging |
LOG_TOOL_FAILURE_PAYLOADS | logToolFailurePayloads | false | Opt-in. Each failed tool call also writes a Tool failure payload: <tool> record carrying toolInput (the arguments as sent) and toolResult (the CallToolResult returned) as redacted JSON strings, at the call's error-record level. Reaches every log destination — stderr, combined.log, and OTLP when OTEL_EXPORTER_OTLP_LOGS_ENDPOINT is set. Redaction is by key name only, so a secret inside a free-form value (a query, a message) is logged. Record shape: api-telemetry Logs |
LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES | logToolFailurePayloadMaxBytes | 16384 | Cap per payload, in UTF-8 bytes. A longer one is cut on a character boundary and flagged with toolInputTruncated / toolResultTruncated |
| Env Var | AppConfig field | Default | Notes |
|---|---|---|---|
MCP_TRANSPORT_TYPE | mcpTransportType | stdio | stdio | http |
MCP_HTTP_PORT | mcpHttpPort | 3010 | Port for HTTP transport |
MCP_HTTP_HOST | mcpHttpHost | 127.0.0.1 | Bind address |
MCP_HTTP_ENDPOINT_PATH | mcpHttpEndpointPath | /mcp | HTTP endpoint path |
MCP_HTTP_MAX_BODY_BYTES | mcpHttpMaxBodyBytes | 1048576 (1 MiB) | Max inbound JSON-RPC request body; oversized requests get 413 before per-request allocation. Does not cap upstream data staged into a canvas or response sizes. 0 disables (defer to runtime/proxy). The only body limit in force — the SDK's own 4 MiB read cap never engages, so a value above 4 MiB holds as set. |
MCP_HTTP_MAX_PORT_RETRIES | mcpHttpMaxPortRetries | 15 | Rungs of the port ladder walked when a bind collides; each rung tries port + 1. See Port binding |
MCP_HTTP_PORT_RETRY_DELAY_MS | mcpHttpPortRetryDelayMs | 50 | Delay between port retries (ms) |
MCP_SESSION_MODE | mcpSessionMode | auto | stateless | stateful | auto; auto resolves to stateful. Under stateless, the 2025-era multi-round-trip shim still runs but its capability gate refuses: each request is served by an instance that never processed initialize, so the client-capability view is empty and a ctx.requestInput round can never be answered — fail-closed, but unconditional, so the tool is unusable for those clients rather than merely guarded. 2026-07-28 clients and stdio are unaffected. Seed it from code with createApp({ sessionMode }) — see below |
MCP_STATEFUL_SESSION_STALE_TIMEOUT_MS | mcpStatefulSessionStaleTimeoutMs | 1800000 | 30 min; stale session eviction |
MCP_HTTP_RESUMABILITY | mcpHttpResumability | true | SSE stream replay under stateful HTTP. On by default — selecting a session mode is the opt-in. Kill switch only; no effect on stateless serving or the session-less 2026-07-28 era |
MCP_HTTP_RESUMABILITY_MAX_EVENTS | mcpHttpResumabilityMaxEvents | 512 | Events retained per session for replay; oldest evicted first. Lower it on a server whose tools return large results |
MCP_HTTP_RESUMABILITY_TTL_MS | mcpHttpResumabilityTtlMs | 300000 | 5 min; how long a retained event stays replayable |
MCP_ALLOWED_ORIGINS | mcpAllowedOrigins | — | Comma-separated list of browser origins the MCP endpoint accepts; others get 403. Omitted, CORS is wildcard but only loopback origins pass; * accepts any origin (disables DNS-rebinding protection). An accepted origin's preflight also allows Mcp-Method, Mcp-Name, Last-Event-ID, and each tool's Mcp-Param-<Name> — see api-auth |
MCP_SERVER_RESOURCE_IDENTIFIER | mcpServerResourceIdentifier | — | RFC 8707 resource indicator URL |
MCP_PUBLIC_URL | mcpPublicUrl | — | Public-facing origin for reverse proxies (Cloudflare Tunnel, nginx, ALB) so emitted URLs carry the correct scheme |
MCP_HEARTBEAT_INTERVAL_MS | mcpHeartbeatIntervalMs | 0 (disabled) | Heartbeat ping interval; 0 disables |
MCP_HEARTBEAT_MISS_THRESHOLD | mcpHeartbeatMissThreshold | 3 | Missed heartbeats before session is considered stale |
MCP_GC_PRESSURE_INTERVAL_MS | mcpGcPressureIntervalMs | 0 (disabled) | Bun-only opt-in forced GC loop for HTTP deployments with heap growth |
MCP_HTTP_PORT is where the HTTP transport starts, not necessarily where it ends up. Startup walks a ladder: bind MCP_HTTP_PORT, and on a collision wait MCP_HTTP_PORT_RETRY_DELAY_MS and try the next port, up to MCP_HTTP_MAX_PORT_RETRIES times. Read the bound port off the HTTP transport listening at … log line or the startup banner — with the defaults the server may be anywhere in 3010–3025. Pin the port by setting MCP_HTTP_MAX_PORT_RETRIES=0, which makes a collision a startup failure instead of a silent move.
'listening'. A bind failure arriving after the listen call — a collision the pre-bind probe could not see, because another process took the port in between — is routed to the ladder like any other, not reported as a successful start.EACCES (privileged port, typically <1024 as a non-root user) and EADDRNOTAVAIL (the MCP_HTTP_HOST address is not local to this machine) fail startup on the first attempt with the OS error as the rejection's cause, rather than burning every rung. Ladder exhaustion carries the last bind error as cause too, when a real bind attempt produced one.EADDRINUSE where Node reports EACCES. On Bun a privileged port therefore reads as an ordinary collision and walks the whole ladder before failing with Failed to bind to any port after N retries. — on Node the same port fails on the first attempt, naming EACCES.| Env Var | AppConfig field | Default | Notes |
|---|---|---|---|
MCP_AUTH_MODE | mcpAuthMode | none | none | jwt | oauth |
MCP_AUTH_SECRET_KEY | mcpAuthSecretKey | — | Required for jwt mode; min 32 chars |
MCP_AUTH_DISABLE_SCOPE_CHECKS | mcpAuthDisableScopeChecks | false | When true, bypasses both withRequiredScopes (declared auth: [...]) and checkScopes (runtime/tenant scopes). Token validation (sig/aud/iss/exp) intact. Logs a WARNING at startup. See api-auth skill. |
MCP_REQUEST_STATE_KEY | mcpRequestStateKey | — | Opt-in, any auth mode. When set, the framework seals the requestState a handler returns with ctx.requestInput (bound to the request's clientId / subject / tenantId, valid 900 s) and every server instance rejects any other state as -32602 invalid_request_state before the handler runs; ctx.inputs.state() still returns the handler's string. Must be ≥ 32 UTF-8 bytes — shorter fails startup with a ConfigurationError naming the variable — and identical on every instance a retry can reach (stateless replicas, Worker isolates, restarts). Unset or empty: no verifier, state round-trips raw. See api-context § requestState |
OAUTH_ISSUER_URL | oauthIssuerUrl | — | Required for oauth mode |
OAUTH_AUDIENCE | oauthAudience | — | Required for oauth mode |
OAUTH_JWKS_URI | oauthJwksUri | — | Override JWKS endpoint (otherwise derived from issuer) |
OAUTH_JWKS_COOLDOWN_MS | oauthJwksCooldownMs | 300000 | 5 min; min time between JWKS refetches |
OAUTH_JWKS_TIMEOUT_MS | oauthJwksTimeoutMs | 5000 | JWKS fetch timeout (ms) |
DEV_MCP_AUTH_BYPASS | devMcpAuthBypass | false | Skip auth in development; blocked in production |
MCP_JWT_EXPECTED_ISSUER | mcpJwtExpectedIssuer | — | Optional issuer validation for JWT mode |
MCP_JWT_EXPECTED_AUDIENCE | mcpJwtExpectedAudience | — | Optional audience validation for JWT mode |
DEV_MCP_CLIENT_ID | devMcpClientId | — | Dev-only: override client ID |
DEV_MCP_SCOPES | devMcpScopes | — | Dev-only: comma-separated scope overrides |
| Env Var | AppConfig field | Default | Notes |
|---|---|---|---|
STORAGE_PROVIDER_TYPE | storage.providerType | in-memory | in-memory | filesystem | supabase | cloudflare-r2 | cloudflare-kv | cloudflare-d1; aliases: mem, fs |
STORAGE_FILESYSTEM_PATH | storage.filesystemPath | ./.storage | Used only when providerType is filesystem |
@duckdb/node-api)| Env Var | AppConfig field | Default | Notes |
|---|---|---|---|
CANVAS_PROVIDER_TYPE | canvas.providerType | none | none | duckdb. Set to duckdb to enable core.canvas. Fails closed on Cloudflare Workers (DuckDB has no V8-isolate build). |
CANVAS_DEFAULT_MEMORY_LIMIT_MB | canvas.defaultMemoryLimitMb | 1024 | Per-canvas DuckDB memory_limit PRAGMA value, in MB. |
CANVAS_EXPORT_PATH | canvas.exportRootPath | ./.canvas-exports | Sandbox root for path-targeted exports. Absolute paths and .. traversal are rejected. |
CANVAS_TEMP_PATH | canvas.tempRootPath | os.tmpdir() | Parent of the provider's private scratch directory: on first use the DuckDB provider creates mcp-canvas-XXXXXX inside it (mkdtemp, 0700 on POSIX) for each canvas's own DuckDB temp_directory and the transient files behind stream exports and importFrom; shutdown removes it once the calls still running against it settle. Created if missing, with no ownership or mode check — must not be a directory another local user controls, and on Windows, where the private directory inherits the parent's ACL, must not grant other users access. Never resolves to the process cwd — DuckDB's own cwd-relative .tmp default fails on a non-root or read-only container rootfs. |
CANVAS_MAX_CANVASES_PER_TENANT | canvas.maxCanvasesPerTenant | 100 | Active canvas cap per tenant; throws RateLimited when exceeded. |
CANVAS_TTL_MS | canvas.ttlMs | 86400000 | Sliding TTL (24 h). Every operation extends the expiry. |
CANVAS_ABSOLUTE_CAP_MS | canvas.absoluteCapMs | 604800000 | Absolute cap from creation (7 d). Sliding window clamps to this. |
CANVAS_SWEEPER_INTERVAL_MS | canvas.sweeperIntervalMs | 60000 | Background sweep interval. Set to 0 to disable. |
CANVAS_DEFAULT_ROW_LIMIT | canvas.defaultRowLimit | 10000 | Default cap on rows materialized into a query response. |
CANVAS_SCHEMA_SNIFF_ROWS | canvas.schemaSniffRows | 100 | Rows to materialize for schema inference when schema is omitted. |
Platform support: Linux/macOS/Windows × x64 supported, Linux/macOS arm64 supported. Windows arm64 unsupported (DuckDB upstream). See api-canvas skill for the full DataCanvas reference.
Activated when SUPABASE_URL is set.
| Env Var | AppConfig field | Notes |
|---|---|---|
SUPABASE_URL | supabase.url | Required to activate |
SUPABASE_SERVICE_ROLE_KEY | supabase.serviceRoleKey | Required by the supabase storage provider (admin client) |
SUPABASE_ANON_KEY | supabase.anonKey | Optional; for the server's own public client — the framework never reads it |
| Env Var | AppConfig field | Default | Notes |
|---|---|---|---|
OPENROUTER_API_KEY | openrouterApiKey | — | Optional; enables LLM provider |
OPENROUTER_APP_URL | openrouterAppUrl | http://localhost:3000 | Reported to OpenRouter |
OPENROUTER_APP_NAME | openrouterAppName | package.json name | Reported to OpenRouter |
LLM_DEFAULT_MODEL | llmDefaultModel | google/gemini-2.5-flash-preview-05-20 | OpenRouter model ID |
LLM_DEFAULT_TEMPERATURE | llmDefaultTemperature | — | Float |
LLM_DEFAULT_TOP_P | llmDefaultTopP | — | Float |
LLM_DEFAULT_MAX_TOKENS | llmDefaultMaxTokens | — | Integer |
LLM_DEFAULT_TOP_K | llmDefaultTopK | — | Integer |
LLM_DEFAULT_MIN_P | llmDefaultMinP | — | Float |
| Env Var | AppConfig field | Default | Notes |
|---|---|---|---|
OTEL_ENABLED | openTelemetry.enabled | false | Enable OpenTelemetry export |
OTEL_SERVICE_NAME | openTelemetry.serviceName | createApp name → package.json name | Seeded from createApp({ name }) when unset; an env value wins |
OTEL_SERVICE_VERSION | openTelemetry.serviceVersion | package.json version | |
OTEL_EXPORTER_OTLP_ENDPOINT | — | — | OTLP/HTTP base URL; resolves tracesEndpoint to <base>/v1/traces and metricsEndpoint to <base>/v1/metrics (path prefix kept) when the signal-specific variable is unset |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | openTelemetry.tracesEndpoint | — | OTLP traces endpoint URL; overrides the base, used as-is |
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | openTelemetry.metricsEndpoint | — | OTLP metrics endpoint URL; overrides the base, used as-is |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | openTelemetry.logsEndpoint | — | OTLP logs endpoint URL; the only switch for log record export, never derived from the base. Needs the optional peers @opentelemetry/sdk-logs, @opentelemetry/exporter-logs-otlp-http, @opentelemetry/api-logs |
OTEL_TRACES_SAMPLER_ARG | openTelemetry.samplingRatio | 1.0 | 0–1; fraction of traces to export |
OTEL_LOG_LEVEL | openTelemetry.logLevel | INFO | OTel SDK internal log level: NONE | ERROR | WARN | INFO | DEBUG | VERBOSE | ALL; aliases warning→WARN, err→ERROR, information→INFO. Diag output goes to stderr at every level |
Activated when SPEECH_TTS_ENABLED or SPEECH_STT_ENABLED is set.
| Env Var | AppConfig field | Default | Notes |
|---|---|---|---|
SPEECH_TTS_ENABLED | speech.tts.enabled | false | Enable TTS |
SPEECH_TTS_PROVIDER | speech.tts.provider | elevenlabs | Currently only elevenlabs |
SPEECH_TTS_API_KEY | speech.tts.apiKey | — | Provider API key |
SPEECH_TTS_BASE_URL | speech.tts.baseUrl | — | Override provider base URL |
SPEECH_TTS_DEFAULT_VOICE_ID | speech.tts.defaultVoiceId | — | Default voice identifier |
SPEECH_TTS_DEFAULT_MODEL_ID | speech.tts.defaultModelId | — | Default model identifier |
SPEECH_TTS_TIMEOUT | speech.tts.timeout | — | Request timeout (ms) |
| Env Var | AppConfig field | Default | Notes |
|---|---|---|---|
SPEECH_STT_ENABLED | speech.stt.enabled | false | Enable STT |
SPEECH_STT_PROVIDER | speech.stt.provider | openai-whisper | Currently only openai-whisper |
SPEECH_STT_API_KEY | speech.stt.apiKey | — | Provider API key |
SPEECH_STT_BASE_URL | speech.stt.baseUrl | — | Override provider base URL |
SPEECH_STT_DEFAULT_MODEL_ID | speech.stt.defaultModelId | — | Default model identifier |
SPEECH_STT_TIMEOUT | speech.stt.timeout | — | Request timeout (ms) |
Define your own Zod schema for domain-specific env vars. Never merge with core's schema.
Use the lazy init/accessor pattern — do not parse process.env at module top-level.
// src/config/server-config.ts
import { z } from '@cyanheads/mcp-ts-core';
import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
const ServerConfigSchema = z.object({
apiKey: z.string().describe('External API key'),
maxResults: z.coerce.number().default(100),
verboseLogging: z.stringbool().default(false).describe('Enable verbose logging'),
});
export type ServerConfig = z.infer<typeof ServerConfigSchema>;
let _config: ServerConfig | undefined;
export function getServerConfig(): ServerConfig {
_config ??= parseEnvConfig(ServerConfigSchema, {
apiKey: 'MY_API_KEY',
maxResults: 'MY_MAX_RESULTS',
verboseLogging: 'MY_VERBOSE_LOGGING',
});
return _config;
}Call getServerConfig() in createApp's setup(). The accessor alone parses on first read, which is usually the first tool call: an invalid value then lets the server report ready and fails every call instead, including tools that never read the bad variable. Calling it in setup() turns that into a startup failure — the ConfigurationError banner naming the variable, exit code 1, before any transport binds. On Workers setup() runs inside the first request, after injectEnvVars(), so the call is safe there too.
setup(core) {
getServerConfig();
initMyService(core.config, core.storage);
},Env booleans — use z.stringbool(), never z.coerce.boolean(). z.coerce.boolean() runs Boolean(value), so "false", "0", and "no" all coerce to true — the flag becomes impossible to disable through the environment except by omitting it entirely. z.stringbool() parses true/false/1/0/yes/no/on/off (case-insensitive) and rejects anything else, so MY_VERBOSE_LOGGING=false actually disables and a typo fails loudly at startup instead of silently coercing. An empty string is not in that accepted set — z.stringbool() rejects '' with Invalid option. What makes a blank .env line take the default is the normalization layer described under Unset means unset below, not the schema type.
Unset means unset. parseEnvConfig and the framework's own config both treat an empty string and a whole-value ${…} placeholder — what an MCPB or plugin host forwards when a user leaves an option blank and nothing substitutes it — as the variable being absent: an optional field stays undefined, a defaulted field takes its default, and a required field fails as missing rather than as a format error against the literal text. A value that merely contains ${…} is kept. No per-field z.preprocess guard is needed for either case.
Why parseEnvConfig? It maps Zod schema paths to env var names so validation errors name the actual variable at fault. A missing MY_API_KEY produces:
Server config validation failed:
- MY_API_KEY (apiKey): Invalid input: expected string, received undefinedInstead of a raw ZodError dump at startup. The framework catches the resulting ConfigurationError and prints a clean banner (full stack behind DEBUG=true).
Direct ServerConfigSchema.parse(...) still works — the framework intercepts raw ZodError thrown from setup() and converts it — but error messages won't know about env var names, so they show the Zod path (apiKey) instead of the variable name (MY_API_KEY). No normalization runs on that path either, so a blank MY_FLAG= arrives as '' and fails validation. normalizeEnv is exported from /config for exactly that case: normalize the values first, then parse.
Workers: Do not parse process.env at module top-level. In Workers, env bindings are injected at request time via injectEnvVars(), after all static imports. Lazy parsing is required.
© cyanheads, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in framework-skills/api-config of cyanheads/pubmed-mcp-server.
Open the folder on GitHubat commit 5a417fb
API Config next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| API Config this skillcyanheads/pubmed-mcp-server | 155 | — | ~6.7k | Automated safety check: Notes | Apache-2.0 | |
| Typescript MCP Server Generatorgithub/awesome-copilot | 40k | — | ~1.7k | Automated safety check: Pass | MIT | |
| MCP Server BuildershareAI-lab/learn-claude-code | 78k | 5 repos | ~1.2k | Automated safety check: Pass | MIT | |
| Qwen Code E2E TestingQwenLM/qwen-code | 28k | — | ~2.1k | Automated safety check: Pass | Apache-2.0 | |
| Yahoo Finance2gadicc/yahoo-finance2 | 801 | — | ~1.8k | Automated safety check: Pass | MIT | |
| Read GitHubAgentTeam-TaichuAI/ScienceClaw | 670 | 2 repos | ~638 | Automated safety check: Pass | None |
github/awesome-copilot
Generate a complete MCP server project in TypeScript using the MCP TypeScript SDK v2 (@modelcontextprotocol/server) with tools, resources, and proper configuration
shareAI-lab/learn-claude-code
Walks through building MCP servers in Python or TypeScript that expose tools, resources and prompts to Claude, with templates, registration and testing.
QwenLM/qwen-code
Guides end-to-end testing of the Qwen Code CLI in headless mode with real model calls, MCP test servers and inspection of raw API traffic.
gadicc/yahoo-finance2
A skill your agent uses when building with the yahoo-finance2 TypeScript/Deno/npm library, using its Yahoo Finance data modules, CLI, or MCP server, or contributing to the yahoo-finance2 repository…
AgentTeam-TaichuAI/ScienceClaw
Read and search GitHub repository documentation via gitmcp.io MCP service.
breaking-brake/cc-wf-studio
Guides edits to cc-wf-studio's workflow schema so AI agents generate better workflows, treating schema text as prompt engineering rather than validation.
cyanheads/pubmed-mcp-server
Scaffold an MCP App tool + UI resource pair. An agent skill from cyanheads/pubmed-mcp-server.
cyanheads/pubmed-mcp-server
Scaffold a new MCP prompt template. An agent skill from cyanheads/pubmed-mcp-server.
cyanheads/pubmed-mcp-server
Scaffold a new MCP resource definition. An agent skill from cyanheads/pubmed-mcp-server.
cyanheads/pubmed-mcp-server
Scaffold a new service integration. An agent skill from cyanheads/pubmed-mcp-server.
cyanheads/pubmed-mcp-server
Scaffold a test file for an existing tool, resource, or service.
cyanheads/pubmed-mcp-server
Authentication, authorization, and multi-tenancy patterns for @cyanheads/mcp-ts-core.
Works with
Categories
Reference for core and server configuration in @cyanheads/mcp-ts-core. API Config is an agent skill from cyanheads/pubmed-mcp-server. Reference for core and server configuration in @cyanheads/mcp-ts-core.
API Config fits situations like: tasks that involve MCP servers.
Run `npx skills add cyanheads/pubmed-mcp-server --skill api-config -a claude-code`. Or copy the skill folder (framework-skills/api-config in cyanheads/pubmed-mcp-server) into .claude/skills/api-config in your project. Claude Code loads it when a task matches its description.
Run `npx skills add cyanheads/pubmed-mcp-server --skill api-config -a codex`. Or copy the skill folder (framework-skills/api-config in cyanheads/pubmed-mcp-server) into .agents/skills/api-config in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add cyanheads/pubmed-mcp-server --skill api-config -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/api-config, .gemini/skills/api-config, .github/skills/api-config and .opencode/skills/api-config in your project.
Going by SKILL.md and its folder, API Config needs credentials named MCP_AUTH_SECRET_KEY, MCP_REQUEST_STATE_KEY, SUPABASE_SERVICE_ROLE_KEY and SUPABASE_ANON_KEY. Our summary lists: Node.js.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.
API Config is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 6.7k tokens (SKILL.md is roughly 27k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with API Config: Typescript MCP Server Generator (github/awesome-copilot, 40k stars), MCP Server Builder (shareAI-lab/learn-claude-code, 78k stars), Qwen Code E2E Testing (QwenLM/qwen-code, 28k stars) and Yahoo Finance2 (gadicc/yahoo-finance2, 801 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
cyanheads (a GitHub user) maintains it in cyanheads/pubmed-mcp-server, which has 155 GitHub stars. The repository holds 30 skills in this directory. The repository was last updated on October 4, 2026.
Source: cyanheads/pubmed-mcp-server on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.