Skip to main content

api-config

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.

Jump to install

Source facts

Repository
cyanheads/obsidian-mcp-server
Last source activity
September 19, 2026 at 15:47
Detected SKILL.md language
English
Stars
682
Forks
103

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
api-config
description
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.
metadata
{"author":"cyanheads","version":"1.19","audience":"external","type":"reference"}
## Overview 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`. --- ## 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):** 1. `name`/`version`/`title`/`websiteUrl`/`description`/`icons` options passed to `createApp()` or `createWorkerHandler()` 2. Environment variables 3. `sessionMode.default` passed to `createApp()` — a default, so it sits *below* the env var it seeds, unlike the identity options above 4. `package.json` fields **Where `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. --- ### Identity | 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 | #### Cache hints (`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`. ```ts 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). - A resource's own `cacheHint` overrides the `resources/read` entry for that resource, field by field — see the `add-resource` skill. - Omitting a hint keeps the SDK defaults (`ttlMs: 0`, `cacheScope: 'private'`). Responses to 2025-era clients are never affected. #### Session mode (`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. ```ts await createApp({ sessionMode: 'stateless' }); // default only await createApp({ sessionMode: { default: 'stateful', require: 'stateful' } }); // and enforced ``` - **`default`** 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. - The advertised `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`. - Cloudflare Workers are outside this contract: `MCP_SESSION_MODE` is not in `CORE_ENV_BINDINGS`, so a `[vars]` entry reaches `process.env` only through `extraEnvBindings`. --- ### Environment & logging | Env Var | `AppConfig` field | Default | Notes | |:--------|:-----------------|:--------|:------| | `NODE_ENV` | `environment` | `development` | Aliases: `dev`→`development`, `prod`→`production`, `test`→`testing` | | `MCP_LOG_LEVEL` | `logLevel` | `debug` | 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 | ### Transport | 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). | | `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](#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; omit to allow all | | `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 | #### Port binding `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. - **Startup resolves only once the server reports `'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. - **A failure the ladder cannot clear rejects immediately.** Each rung only changes the port, so `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. - **Runtime caveat:** Bun reports a permission-denied bind as `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`. --- ### Auth | 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. | | `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 | --- ### Storage | 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` |
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub