@cyanheads/noaa-marine-mcp-server 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/AGENTS.md +33 -16
  2. package/CLAUDE.md +33 -16
  3. package/Dockerfile +9 -3
  4. package/README.md +23 -97
  5. package/changelog/0.6.x/0.6.2.md +27 -0
  6. package/changelog/0.7.x/0.7.0.md +28 -0
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp-server/tools/definitions/noaa-marine-find-stations.tool.d.ts.map +1 -1
  10. package/dist/mcp-server/tools/definitions/noaa-marine-find-stations.tool.js +14 -8
  11. package/dist/mcp-server/tools/definitions/noaa-marine-find-stations.tool.js.map +1 -1
  12. package/dist/mcp-server/tools/definitions/noaa-marine-get-conditions.tool.d.ts.map +1 -1
  13. package/dist/mcp-server/tools/definitions/noaa-marine-get-conditions.tool.js +2 -1
  14. package/dist/mcp-server/tools/definitions/noaa-marine-get-conditions.tool.js.map +1 -1
  15. package/dist/mcp-server/tools/definitions/noaa-marine-get-current-profile.tool.d.ts.map +1 -1
  16. package/dist/mcp-server/tools/definitions/noaa-marine-get-current-profile.tool.js +2 -1
  17. package/dist/mcp-server/tools/definitions/noaa-marine-get-current-profile.tool.js.map +1 -1
  18. package/dist/mcp-server/tools/definitions/noaa-marine-get-currents.tool.d.ts.map +1 -1
  19. package/dist/mcp-server/tools/definitions/noaa-marine-get-currents.tool.js +7 -3
  20. package/dist/mcp-server/tools/definitions/noaa-marine-get-currents.tool.js.map +1 -1
  21. package/dist/mcp-server/tools/definitions/noaa-marine-get-monthly-means.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/noaa-marine-get-monthly-means.tool.js +3 -2
  23. package/dist/mcp-server/tools/definitions/noaa-marine-get-monthly-means.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/noaa-marine-get-ocean-observations.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/noaa-marine-get-ocean-observations.tool.js +2 -1
  26. package/dist/mcp-server/tools/definitions/noaa-marine-get-ocean-observations.tool.js.map +1 -1
  27. package/dist/mcp-server/tools/definitions/noaa-marine-get-tide-predictions.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/noaa-marine-get-tide-predictions.tool.js +4 -3
  29. package/dist/mcp-server/tools/definitions/noaa-marine-get-tide-predictions.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/noaa-marine-get-water-level.tool.d.ts.map +1 -1
  31. package/dist/mcp-server/tools/definitions/noaa-marine-get-water-level.tool.js +5 -4
  32. package/dist/mcp-server/tools/definitions/noaa-marine-get-water-level.tool.js.map +1 -1
  33. package/package.json +11 -10
  34. package/server.json +3 -3
  35. package/skills/noaa-marine-guide/SKILL.md +63 -0
package/AGENTS.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** noaa-marine-mcp-server
4
- **Version:** 0.6.1
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.14`
4
+ **Version:** 0.7.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0 (protocol revisions 2026-07-28 and 2025-*)
8
- **Zod:** ^4.6.5
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0 (resolved 2.4.0; protocol revisions 2026-07-28 and 2025-*)
8
+ **Zod:** ^4.6.7
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
11
11
 
@@ -151,6 +151,8 @@ export function getServerConfig() {
151
151
 
152
152
  `parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`NOAA_APPLICATION_ID`) not the path (`applicationId`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
153
153
 
154
+ For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag cannot be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else.
155
+
154
156
  ### Server identity and instructions
155
157
 
156
158
  This server's `createApp()` identity is the bare repository name in both `name` and `title`: `noaa-marine-mcp-server`. Never add `description` or `websiteUrl` — the framework derives canonical metadata from `package.json`, and a copy in `createApp()` is drift. `instructions` is optional server-level orientation sent to clients on every `initialize`; use it for deployment guidance and cross-tool workflows instead of repeating the same context across tool descriptions.
@@ -159,7 +161,7 @@ This server's `createApp()` identity is the bare repository name in both `name`
159
161
 
160
162
  `createApp({ sessionMode: 'stateless' })` declares the HTTP session posture in `src/` rather than leaving it to a deployment's `MCP_SESSION_MODE`, which still wins whenever it carries a meaningful value (an empty string and an unsubstituted `${…}` placeholder read as unset and fall through to the option). Stateless is correct here: no tool gates on `ctx.requestInput`. A server that does gain one declares `{ default: 'stateful', require: 'stateful' }` so startup fails with a `ConfigurationError` instead of serving a mode a 2025-era HTTP client can never answer in. Stdio is never refused.
161
163
 
162
- `teardown(core)` is the `setup()` counterpart — it releases a watcher, socket, or non-`unref()`'d timer after the transport stops and before the logger closes. Neither service here allocates one (both hold an in-memory station cache and nothing else), so the hook is unused; add it the moment a service acquires a handle it must close. `SIGTERM`/`SIGINT` exit the process explicitly once shutdown settles.
164
+ `teardown(core)` is the `setup()` counterpart — it releases a watcher, socket, or non-`unref()`'d timer after the transport stops and before the logger closes. Neither service here allocates one (both hold an in-memory station cache and nothing else), so the hook is unused; add it the moment a service acquires a handle it must close. `SIGTERM`/`SIGINT` exit the process explicitly once shutdown settles (0, or 1 if a step does not settle within the framework's 10-second ceiling).
163
165
 
164
166
  ---
165
167
 
@@ -169,13 +171,13 @@ Handlers receive a unified `ctx` object. Key properties:
169
171
 
170
172
  | Property | Description |
171
173
  |:---------|:------------|
172
- | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino and `notifications/message`, so treat it as client-visible. |
174
+ | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
173
175
  | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
174
- | `ctx.requestInput` | Suspend and ask the caller for more input. Return it; never await it. Always present, though this server currently has no call sites. |
175
- | `ctx.inputs` | The request's responses, limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked (see the `api-context` skill). |
176
- | `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Never a reason to skip a consent prompt. |
177
- | `ctx.enrich` | Success-path agent context that reaches both `structuredContent` and `content[]` when the definition declares `enrichment`. |
178
- | `ctx.content` | Non-text image/audio content blocks prepended to `content[]`; never enters `structuredContent`. |
176
+ | `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
177
+ | `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |
178
+ | `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |
179
+ | `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
180
+ | `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
179
181
  | `ctx.signal` | `AbortSignal` for cancellation. |
180
182
  | `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
181
183
  | `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
@@ -219,7 +221,7 @@ throw new Error('Invalid query format'); // → ValidationError
219
221
 
220
222
  // McpError — when no factory exists for the code
221
223
  import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
222
- throw new McpError(JsonRpcErrorCode.DatabaseError, 'Connection failed', { pool: 'primary' });
224
+ throw new McpError(JsonRpcErrorCode.InitializationFailed, 'Connection failed', { pool: 'primary' });
223
225
  ```
224
226
 
225
227
  See framework CLAUDE.md and the `api-errors` skill for the full auto-classification table, all available factories, and the contract reference.
@@ -274,9 +276,18 @@ src/
274
276
 
275
277
  ## Skills
276
278
 
277
- Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant for those agents.
279
+ Two skill trees live at the project root, written for different readers:
280
+
281
+ | Tree | For | Written by |
282
+ |:-----|:----|:-----------|
283
+ | `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |
284
+ | `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. Absent by default; usually none is needed |
285
+
286
+ Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.
278
287
 
279
- **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
288
+ A published skill in root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package and the Docker image, and reaches MCP clients when the server opts in with `createApp({ skills: 'skills' })`. `lint:packaging` validates the tree.
289
+
290
+ **Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
280
291
 
281
292
  Available skills:
282
293
 
@@ -288,6 +299,7 @@ Available skills:
288
299
  | `add-app-tool` | Scaffold an MCP App tool + paired UI resource |
289
300
  | `add-resource` | Scaffold a new resource definition |
290
301
  | `add-prompt` | Scaffold a new prompt definition |
302
+ | `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |
291
303
  | `add-service` | Scaffold a new service integration |
292
304
  | `add-test` | Scaffold test file for a tool, resource, or service |
293
305
  | `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
@@ -335,7 +347,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
335
347
  | `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
336
348
  | `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
337
349
  | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
338
- | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, README version-badge parity (run by devcheck) |
350
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, README version-badge parity, `.mcpbignore` coverage, plugin manifests, the published `skills/` tree (run by devcheck) |
339
351
  | `bun run list-skills` | Print the skill registry |
340
352
  | `bun run tree` | Generate directory structure doc |
341
353
  | `bun run format` | Auto-fix formatting (safe fixes only) |
@@ -349,9 +361,11 @@ When you complete a skill's checklist, check the boxes and add a completion time
349
361
 
350
362
  ---
351
363
 
364
+ **CI is one file.** `.github/workflows/codeql.yml` is the only GitHub Actions workflow: CodeQL runs only while the repo's CodeQL default setup is turned off. Verification — `devcheck`, tests, and release gates — runs locally.
365
+
352
366
  ## Bundling
353
367
 
354
- `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
368
+ `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips four classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/` at a package's root, and `SKILL.md` files — a `skills/` deeper in a package, such as the MCP SDK's `dist/ext/skills/`, is runtime code and stays); platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on; node-gyp build output beside a `binding.gyp` (`config.gypi`, Makefiles, `obj.target/`, `.deps/`, object files and debug symbols), which records the packing machine's paths — compiled `*.node` addons stay; and build-only package files — the framework's `changelog/`, `scripts/`, `templates/`, `CLAUDE.md`, `AGENTS.md`, and config bases (`biome.json`, `vitest.config.base.mjs`, `config/tsconfig.base.json`), and `better-sqlite3`'s `deps/` and `src/` C sources. `tests/scripts/bundle-agent-docs.test.ts` keeps the agent-doc matcher in `scripts/clean-mcpb.ts` and `scripts/lint-packaging.ts` identical; a framework script resync that widens it again breaks that test. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
355
369
 
356
370
  **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
357
371
 
@@ -418,7 +432,10 @@ import { getMyService } from '@/services/my-domain/my-service.js';
418
432
  - [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
419
433
  - [ ] Registered in `createApp()` arrays (directly or via barrel exports)
420
434
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
435
+ - [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = the unscoped repo name (lowercase `a-z`, `0-9`, `-`, `.` only); `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` from `package.json`; no other top-level field, `extensions` included. `mcp.json` holds one `"type": "stdio"` entry keyed by the unscoped name, `npx -y <full package name>`, and the same fixed `env` as `.codex-plugin/mcp.json`. Neither file carries a user-supplied variable (no `""`, no `${…}` besides `${PLUGIN_ROOT}`/`${PLUGIN_DATA}`): keys reach Codex through `.codex-plugin/mcp.json` `env_vars`, which works only while `.codex-plugin/plugin.json` keeps `"mcpServers": "./.codex-plugin/mcp.json"`
421
436
  - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
422
437
  - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
423
438
  - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
439
+ - [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass
440
+ - [ ] If the server publishes a skill in root `skills/`: `lint:packaging` passes, and `field-test` verified every gate and identifier claim in it against live tool behavior
424
441
  - [ ] `bun run devcheck` passes
package/CLAUDE.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** noaa-marine-mcp-server
4
- **Version:** 0.6.1
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.14`
4
+ **Version:** 0.7.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0 (protocol revisions 2026-07-28 and 2025-*)
8
- **Zod:** ^4.6.5
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0 (resolved 2.4.0; protocol revisions 2026-07-28 and 2025-*)
8
+ **Zod:** ^4.6.7
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
11
11
 
@@ -151,6 +151,8 @@ export function getServerConfig() {
151
151
 
152
152
  `parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`NOAA_APPLICATION_ID`) not the path (`applicationId`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
153
153
 
154
+ For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag cannot be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else.
155
+
154
156
  ### Server identity and instructions
155
157
 
156
158
  This server's `createApp()` identity is the bare repository name in both `name` and `title`: `noaa-marine-mcp-server`. Never add `description` or `websiteUrl` — the framework derives canonical metadata from `package.json`, and a copy in `createApp()` is drift. `instructions` is optional server-level orientation sent to clients on every `initialize`; use it for deployment guidance and cross-tool workflows instead of repeating the same context across tool descriptions.
@@ -159,7 +161,7 @@ This server's `createApp()` identity is the bare repository name in both `name`
159
161
 
160
162
  `createApp({ sessionMode: 'stateless' })` declares the HTTP session posture in `src/` rather than leaving it to a deployment's `MCP_SESSION_MODE`, which still wins whenever it carries a meaningful value (an empty string and an unsubstituted `${…}` placeholder read as unset and fall through to the option). Stateless is correct here: no tool gates on `ctx.requestInput`. A server that does gain one declares `{ default: 'stateful', require: 'stateful' }` so startup fails with a `ConfigurationError` instead of serving a mode a 2025-era HTTP client can never answer in. Stdio is never refused.
161
163
 
162
- `teardown(core)` is the `setup()` counterpart — it releases a watcher, socket, or non-`unref()`'d timer after the transport stops and before the logger closes. Neither service here allocates one (both hold an in-memory station cache and nothing else), so the hook is unused; add it the moment a service acquires a handle it must close. `SIGTERM`/`SIGINT` exit the process explicitly once shutdown settles.
164
+ `teardown(core)` is the `setup()` counterpart — it releases a watcher, socket, or non-`unref()`'d timer after the transport stops and before the logger closes. Neither service here allocates one (both hold an in-memory station cache and nothing else), so the hook is unused; add it the moment a service acquires a handle it must close. `SIGTERM`/`SIGINT` exit the process explicitly once shutdown settles (0, or 1 if a step does not settle within the framework's 10-second ceiling).
163
165
 
164
166
  ---
165
167
 
@@ -169,13 +171,13 @@ Handlers receive a unified `ctx` object. Key properties:
169
171
 
170
172
  | Property | Description |
171
173
  |:---------|:------------|
172
- | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino and `notifications/message`, so treat it as client-visible. |
174
+ | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
173
175
  | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
174
- | `ctx.requestInput` | Suspend and ask the caller for more input. Return it; never await it. Always present, though this server currently has no call sites. |
175
- | `ctx.inputs` | The request's responses, limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked (see the `api-context` skill). |
176
- | `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Never a reason to skip a consent prompt. |
177
- | `ctx.enrich` | Success-path agent context that reaches both `structuredContent` and `content[]` when the definition declares `enrichment`. |
178
- | `ctx.content` | Non-text image/audio content blocks prepended to `content[]`; never enters `structuredContent`. |
176
+ | `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
177
+ | `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |
178
+ | `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |
179
+ | `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
180
+ | `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
179
181
  | `ctx.signal` | `AbortSignal` for cancellation. |
180
182
  | `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
181
183
  | `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
@@ -219,7 +221,7 @@ throw new Error('Invalid query format'); // → ValidationError
219
221
 
220
222
  // McpError — when no factory exists for the code
221
223
  import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
222
- throw new McpError(JsonRpcErrorCode.DatabaseError, 'Connection failed', { pool: 'primary' });
224
+ throw new McpError(JsonRpcErrorCode.InitializationFailed, 'Connection failed', { pool: 'primary' });
223
225
  ```
224
226
 
225
227
  See framework CLAUDE.md and the `api-errors` skill for the full auto-classification table, all available factories, and the contract reference.
@@ -274,9 +276,18 @@ src/
274
276
 
275
277
  ## Skills
276
278
 
277
- Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant for those agents.
279
+ Two skill trees live at the project root, written for different readers:
280
+
281
+ | Tree | For | Written by |
282
+ |:-----|:----|:-----------|
283
+ | `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |
284
+ | `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. Absent by default; usually none is needed |
285
+
286
+ Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.
278
287
 
279
- **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
288
+ A published skill in root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package and the Docker image, and reaches MCP clients when the server opts in with `createApp({ skills: 'skills' })`. `lint:packaging` validates the tree.
289
+
290
+ **Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
280
291
 
281
292
  Available skills:
282
293
 
@@ -288,6 +299,7 @@ Available skills:
288
299
  | `add-app-tool` | Scaffold an MCP App tool + paired UI resource |
289
300
  | `add-resource` | Scaffold a new resource definition |
290
301
  | `add-prompt` | Scaffold a new prompt definition |
302
+ | `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |
291
303
  | `add-service` | Scaffold a new service integration |
292
304
  | `add-test` | Scaffold test file for a tool, resource, or service |
293
305
  | `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
@@ -335,7 +347,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
335
347
  | `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
336
348
  | `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
337
349
  | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
338
- | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, README version-badge parity (run by devcheck) |
350
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, README version-badge parity, `.mcpbignore` coverage, plugin manifests, the published `skills/` tree (run by devcheck) |
339
351
  | `bun run list-skills` | Print the skill registry |
340
352
  | `bun run tree` | Generate directory structure doc |
341
353
  | `bun run format` | Auto-fix formatting (safe fixes only) |
@@ -349,9 +361,11 @@ When you complete a skill's checklist, check the boxes and add a completion time
349
361
 
350
362
  ---
351
363
 
364
+ **CI is one file.** `.github/workflows/codeql.yml` is the only GitHub Actions workflow: CodeQL runs only while the repo's CodeQL default setup is turned off. Verification — `devcheck`, tests, and release gates — runs locally.
365
+
352
366
  ## Bundling
353
367
 
354
- `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
368
+ `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips four classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/` at a package's root, and `SKILL.md` files — a `skills/` deeper in a package, such as the MCP SDK's `dist/ext/skills/`, is runtime code and stays); platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on; node-gyp build output beside a `binding.gyp` (`config.gypi`, Makefiles, `obj.target/`, `.deps/`, object files and debug symbols), which records the packing machine's paths — compiled `*.node` addons stay; and build-only package files — the framework's `changelog/`, `scripts/`, `templates/`, `CLAUDE.md`, `AGENTS.md`, and config bases (`biome.json`, `vitest.config.base.mjs`, `config/tsconfig.base.json`), and `better-sqlite3`'s `deps/` and `src/` C sources. `tests/scripts/bundle-agent-docs.test.ts` keeps the agent-doc matcher in `scripts/clean-mcpb.ts` and `scripts/lint-packaging.ts` identical; a framework script resync that widens it again breaks that test. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
355
369
 
356
370
  **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
357
371
 
@@ -418,7 +432,10 @@ import { getMyService } from '@/services/my-domain/my-service.js';
418
432
  - [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
419
433
  - [ ] Registered in `createApp()` arrays (directly or via barrel exports)
420
434
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
435
+ - [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = the unscoped repo name (lowercase `a-z`, `0-9`, `-`, `.` only); `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` from `package.json`; no other top-level field, `extensions` included. `mcp.json` holds one `"type": "stdio"` entry keyed by the unscoped name, `npx -y <full package name>`, and the same fixed `env` as `.codex-plugin/mcp.json`. Neither file carries a user-supplied variable (no `""`, no `${…}` besides `${PLUGIN_ROOT}`/`${PLUGIN_DATA}`): keys reach Codex through `.codex-plugin/mcp.json` `env_vars`, which works only while `.codex-plugin/plugin.json` keeps `"mcpServers": "./.codex-plugin/mcp.json"`
421
436
  - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
422
437
  - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
423
438
  - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
439
+ - [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass
440
+ - [ ] If the server publishes a skill in root `skills/`: `lint:packaging` passes, and `field-test` verified every gate and identifier claim in it against live tool behavior
424
441
  - [ ] `bun run devcheck` passes
package/Dockerfile CHANGED
@@ -14,7 +14,7 @@
14
14
  # output. A stage that compiles a native addon needs the target-arch toolchain
15
15
  # and cannot cross-compile this way — drop the flag there.
16
16
  # ==============================================================================
17
- FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS build
17
+ FROM --platform=$BUILDPLATFORM oven/bun:1.4.4 AS build
18
18
 
19
19
  WORKDIR /usr/src/app
20
20
 
@@ -29,6 +29,11 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
29
29
  # Copy the rest of the source code
30
30
  COPY . .
31
31
 
32
+ # The skills a server publishes (`createApp({ skills: 'skills' })`) are read at
33
+ # runtime from the application root. Created when the project has none, so the
34
+ # production stage's copy below holds with or without one.
35
+ RUN mkdir -p skills
36
+
32
37
  # Build the application
33
38
  RUN bun run build
34
39
 
@@ -46,7 +51,7 @@ RUN bun run build
46
51
  # A clean image rather than `FROM build`: the build stage's node_modules holds
47
52
  # devDependencies.
48
53
  # ==============================================================================
49
- FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS deps
54
+ FROM --platform=$BUILDPLATFORM oven/bun:1.4.4 AS deps
50
55
 
51
56
  WORKDIR /usr/src/app
52
57
 
@@ -114,7 +119,7 @@ RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
114
119
  # dependencies and build artifacts. Its only Bun invocations are HEALTHCHECK
115
120
  # and CMD, which run on the real target at container start.
116
121
  # ==============================================================================
117
- FROM oven/bun:1.4.2-slim AS production
122
+ FROM oven/bun:1.4.4-slim AS production
118
123
 
119
124
  WORKDIR /usr/src/app
120
125
 
@@ -136,6 +141,7 @@ COPY --from=deps /usr/src/app/node_modules ./node_modules
136
141
 
137
142
  # Copy the compiled application code from the build stage
138
143
  COPY --from=build /usr/src/app/dist ./dist
144
+ COPY --from=build /usr/src/app/skills ./skills
139
145
 
140
146
  # The 'oven/bun' image already provides a non-root user named 'bun'.
141
147
  # We will use this existing user for enhanced security.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.6.1-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/noaa-marine-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.2.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/noaa-marine-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/noaa-marine-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.2-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.7.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/noaa-marine-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.4.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/noaa-marine-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/noaa-marine-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.4-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -29,7 +29,7 @@
29
29
 
30
30
  ## Overview
31
31
 
32
- US tides, currents, water levels, and buoy observations from NOAA CO-OPS and NDBC. Find coastal stations and buoys, then fetch tide and tidal-current predictions, observed and monthly-mean water levels, and live buoy conditions, current profiles, and water-column readings. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
32
+ US tides, currents, water levels, and buoy observations from NOAA CO-OPS and NDBC. Find coastal stations and buoys, then fetch tide and tidal-current predictions, observed and monthly-mean water levels, and live buoy conditions, current profiles, and water-column readings.
33
33
 
34
34
  ### Tools
35
35
 
@@ -140,115 +140,32 @@ Agent-friendly output:
140
140
 
141
141
  ## Getting started
142
142
 
143
- ### Public Hosted Instance
143
+ Connect to the hosted instance, no install:
144
144
 
145
- A public instance is available at `https://noaa-marine.caseyjhand.com/mcp`, with no installation or API key. Point any MCP client at it via Streamable HTTP:
146
-
147
- ```json
148
- {
149
- "mcpServers": {
150
- "noaa-marine-mcp-server": {
151
- "type": "streamable-http",
152
- "url": "https://noaa-marine.caseyjhand.com/mcp"
153
- }
154
- }
155
- }
145
+ ```sh
146
+ claude mcp add --transport http noaa-marine-mcp-server https://noaa-marine.caseyjhand.com/mcp
156
147
  ```
157
148
 
158
- ### Self-Hosted / Local
149
+ Or run it locally:
159
150
 
160
- Add the following to your MCP client configuration file:
161
-
162
- ```json
163
- {
164
- "mcpServers": {
165
- "noaa-marine-mcp-server": {
166
- "type": "stdio",
167
- "command": "bunx",
168
- "args": ["@cyanheads/noaa-marine-mcp-server@latest"],
169
- "env": {
170
- "MCP_TRANSPORT_TYPE": "stdio",
171
- "MCP_LOG_LEVEL": "info"
172
- }
173
- }
174
- }
175
- }
151
+ ```sh
152
+ claude mcp add noaa-marine-mcp-server -- npx -y @cyanheads/noaa-marine-mcp-server
176
153
  ```
177
154
 
178
- Or with npx (no Bun required):
155
+ For other clients, add this to the MCP config file:
179
156
 
180
157
  ```json
181
158
  {
182
159
  "mcpServers": {
183
160
  "noaa-marine-mcp-server": {
184
- "type": "stdio",
185
161
  "command": "npx",
186
- "args": ["-y", "@cyanheads/noaa-marine-mcp-server@latest"],
187
- "env": {
188
- "MCP_TRANSPORT_TYPE": "stdio",
189
- "MCP_LOG_LEVEL": "info"
190
- }
191
- }
192
- }
193
- }
194
- ```
195
-
196
- Or with Docker:
197
-
198
- ```json
199
- {
200
- "mcpServers": {
201
- "noaa-marine-mcp-server": {
202
- "type": "stdio",
203
- "command": "docker",
204
- "args": [
205
- "run", "-i", "--rm",
206
- "-e", "MCP_TRANSPORT_TYPE=stdio",
207
- "ghcr.io/cyanheads/noaa-marine-mcp-server:latest"
208
- ]
162
+ "args": ["-y", "@cyanheads/noaa-marine-mcp-server"]
209
163
  }
210
164
  }
211
165
  }
212
166
  ```
213
167
 
214
- For Streamable HTTP, set the transport and start the server:
215
-
216
- ```sh
217
- MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
218
- # Server listens at http://localhost:3010/mcp
219
- ```
220
-
221
- ### Prerequisites
222
-
223
- - [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
224
- - No API key: NOAA CO-OPS and NDBC are open data sources.
225
-
226
- ### Installation
227
-
228
- 1. **Clone the repository:**
229
-
230
- ```sh
231
- git clone https://github.com/cyanheads/noaa-marine-mcp-server.git
232
- ```
233
-
234
- 2. **Navigate into the directory:**
235
-
236
- ```sh
237
- cd noaa-marine-mcp-server
238
- ```
239
-
240
- 3. **Install dependencies:**
241
-
242
- ```sh
243
- bun install
244
- ```
245
-
246
- 4. **Configure environment:**
247
-
248
- ```sh
249
- cp .env.example .env
250
- # edit .env if needed (all vars optional)
251
- ```
168
+ No API key: NOAA CO-OPS and NDBC are open data sources. For Docker or a source build, see [Running the server](#running-the-server).
252
169
 
253
170
  ## Configuration
254
171
 
@@ -273,6 +190,16 @@ See [`.env.example`](./.env.example) for the full list of optional overrides.
273
190
 
274
191
  ### Local development
275
192
 
193
+ Requires [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
194
+
195
+ - **Clone and install:**
196
+
197
+ ```sh
198
+ git clone https://github.com/cyanheads/noaa-marine-mcp-server.git
199
+ cd noaa-marine-mcp-server && bun install
200
+ cp .env.example .env # all vars optional
201
+ ```
202
+
276
203
  - **Build and run:**
277
204
 
278
205
  ```sh
@@ -294,11 +221,10 @@ See [`.env.example`](./.env.example) for the full list of optional overrides.
294
221
  ### Docker
295
222
 
296
223
  ```sh
297
- docker build -t noaa-marine-mcp-server .
298
- docker run --rm -p 3010:3010 noaa-marine-mcp-server
224
+ docker run --rm -p 3010:3010 ghcr.io/cyanheads/noaa-marine-mcp-server:latest
299
225
  ```
300
226
 
301
- The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `/var/log/noaa-marine-mcp-server`. OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
227
+ The image defaults to HTTP transport, stateless session mode, and logs to `/var/log/noaa-marine-mcp-server`. To run it over stdio from an MCP client, use `"command": "docker"` with `"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/noaa-marine-mcp-server:latest"]`. To build locally, `docker build -t noaa-marine-mcp-server .`; OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
302
228
 
303
229
  ## Project structure
304
230
 
@@ -0,0 +1,27 @@
1
+ ---
2
+ summary: "Marine tool responses preserve literal station names, metadata, queries, and notices in Markdown, with framework 0.13.16 and refreshed dependencies."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.6.2 — 2026-10-25
8
+
9
+ ## Changed
10
+
11
+ - **Framework error and enrichment text** now escapes Markdown syntax in `content[]`, preserving literal messages, recovery hints, and trailer values.
12
+ - **Maintenance tooling and guidance** refresh audit attribution, bundle exclusions, configuration references, and hosted-client connection examples.
13
+
14
+ ## Fixed
15
+
16
+ - **Marine tool Markdown** preserves literal upstream station names and metadata across all eight tools, including caller queries and discovery/current notices.
17
+
18
+ ## Dependencies
19
+
20
+ - `@cyanheads/mcp-ts-core` ^0.13.14 → ^0.13.16.
21
+ - `zod` ^4.6.5 → ^4.6.7 (GHSA-mrhx-c7xc-m3w5).
22
+ - `@biomejs/biome` ^2.5.15 → ^2.6.3.
23
+ - `@types/node` ^26.6.4 → ^26.6.5.
24
+ - `ignore` ^7.0.12 → ^7.0.14.
25
+ - `tsc-alias` ^1.9.7 → ^1.9.9.
26
+ - `vitest` ^5.0.3 → ^5.0.5.
27
+ - **Bun** 1.4.2 → 1.4.4 (`packageManager` and Docker images).
@@ -0,0 +1,28 @@
1
+ ---
2
+ summary: "Publishes the noaa-marine-guide skill over MCP, fixes the per-request HTTP memory retention that framework 0.13.16 introduced, and moves to framework 0.13.18."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.7.0 — 2026-10-28
8
+
9
+ ## Added
10
+
11
+ - **`noaa-marine-guide` skill** is served over MCP through `createApp({ skills })` (framework 0.13.18). `resources/list` gains `skill://noaa-marine-guide/SKILL.md`, `resources/templates/list` gains `skill://{+path}`, and `initialize` capabilities advertise the `io.modelcontextprotocol/skills` extension. The skill also ships in the npm package, the Docker image, and the `.mcpb`.
12
+ - **Agent Plugins** root `plugin.json` and `mcp.json`.
13
+
14
+ ## Changed
15
+
16
+ - **`noaa_marine_find_stations` description and the server `instructions`** now say `query` matches station names and IDs, not places, so a town or marina needs geocoding to coordinates first. They previously said the tool resolves "a place name".
17
+ - **Enrichment trailer string values** render in `content[]` as code literals (framework 0.13.18), e.g. ``**predictions_status:** `empty` `` from `noaa_marine_get_water_level` at a station with no tide predictions. `structuredContent` is unchanged.
18
+ - **The `.mcpb`** no longer carries the framework's build-only files (framework 0.13.18), and keeps the MCP SDK's `dist/ext/skills/` runtime that serving skills needs, via a local fix to the copied bundle cleaner and packaging check ([cyanheads/mcp-ts-core#794](https://github.com/cyanheads/mcp-ts-core/issues/794)).
19
+ - **README Getting started** is restructured: hosted `claude mcp add`, local `npx`, one stdio JSON block; Docker uses the published image.
20
+ - **Maintenance:** `docs/design.md` corrections; framework skills re-synced; `bunfig.toml` release-age guard set to 1 day; the bug-report form uses one Environment table.
21
+
22
+ ## Fixed
23
+
24
+ - **HTTP transport no longer retains memory per request.** Over Streamable HTTP, framework 0.13.16 (shipped in 0.6.2) held about 71 KiB per request through its shutdown signal, so heap grew with sustained traffic. Framework 0.13.17 releases each request's signal when its response ends ([cyanheads/mcp-ts-core#786](https://github.com/cyanheads/mcp-ts-core/issues/786)).
25
+
26
+ ## Dependencies
27
+
28
+ - `@cyanheads/mcp-ts-core` ^0.13.16 → ^0.13.18.
package/dist/index.js CHANGED
@@ -32,12 +32,13 @@ await createApp({
32
32
  ],
33
33
  resources: [noaaMarineStationResource],
34
34
  prompts: [],
35
+ skills: 'skills',
35
36
  cacheHints: {
36
37
  'tools/list': { ttlMs: 86_400_000, cacheScope: 'public' },
37
38
  'resources/list': { ttlMs: 86_400_000, cacheScope: 'public' },
38
39
  'resources/templates/list': { ttlMs: 86_400_000, cacheScope: 'public' },
39
40
  },
40
- instructions: `US marine conditions from NOAA CO-OPS and NDBC: CO-OPS is coastal-gauge tide and tidal-current predictions plus observed water levels, NDBC is live buoy observation, and every water height is referenced to MLLW, the US nautical chart datum, unless another datum is requested. Start with noaa_marine_find_stations to resolve a place name, a coordinate pair, or a bare station number to a station ID — its types filter names what each station can serve, and an ID from one source never works on a tool that reads the other. That source split is also what separates the two current tools: noaa_marine_get_currents returns CO-OPS tidal-current predictions, noaa_marine_get_current_profile an observed NDBC ADCP measurement.`,
41
+ instructions: `US marine conditions from NOAA CO-OPS and NDBC: CO-OPS is coastal-gauge tide and tidal-current predictions plus observed water levels, NDBC is live buoy observation, and every water height is referenced to MLLW, the US nautical chart datum, unless another datum is requested. Start with noaa_marine_find_stations to resolve a coordinate pair, a station name, or a bare station number to a station ID (it matches station names, not places, so geocode a town or marina to coordinates first) — its types filter names what each station can serve, and an ID from one source never works on a tool that reads the other. That source split is also what separates the two current tools: noaa_marine_get_currents returns CO-OPS tidal-current predictions, noaa_marine_get_current_profile an observed NDBC ADCP measurement.`,
41
42
  setup(core) {
42
43
  const serverConfig = getServerConfig();
43
44
  initCoopsService(core.config, core.storage, serverConfig);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,oEAAoE,CAAC;AAC/G,OAAO,EAAE,sBAAsB,EAAE,MAAM,kEAAkE,CAAC;AAC1G,OAAO,EAAE,uBAAuB,EAAE,MAAM,mEAAmE,CAAC;AAC5G,OAAO,EAAE,2BAA2B,EAAE,MAAM,wEAAwE,CAAC;AACrH,OAAO,EAAE,qBAAqB,EAAE,MAAM,iEAAiE,CAAC;AACxG,OAAO,EAAE,yBAAyB,EAAE,MAAM,sEAAsE,CAAC;AACjH,OAAO,EAAE,8BAA8B,EAAE,MAAM,2EAA2E,CAAC;AAC3H,OAAO,EAAE,4BAA4B,EAAE,MAAM,yEAAyE,CAAC;AACvH,OAAO,EAAE,uBAAuB,EAAE,MAAM,oEAAoE,CAAC;AAC7G,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAElE,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,wBAAwB;IAC9B,KAAK,EAAE,wBAAwB;IAC/B,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE;QACL,sBAAsB;QACtB,4BAA4B;QAC5B,uBAAuB;QACvB,yBAAyB;QACzB,qBAAqB;QACrB,uBAAuB;QACvB,2BAA2B;QAC3B,8BAA8B;KAC/B;IACD,SAAS,EAAE,CAAC,yBAAyB,CAAC;IACtC,OAAO,EAAE,EAAE;IACX,UAAU,EAAE;QACV,YAAY,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE;QACzD,gBAAgB,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE;QAC7D,0BAA0B,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE;KACxE;IACD,YAAY,EAAE,itBAAitB;IAE/tB,KAAK,CAAC,IAAI;QACR,MAAM,YAAY,GAAG,eAAe,EAAE,CAAC;QACvC,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QAC1D,eAAe,EAAE,CAAC;IACpB,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,oEAAoE,CAAC;AAC/G,OAAO,EAAE,sBAAsB,EAAE,MAAM,kEAAkE,CAAC;AAC1G,OAAO,EAAE,uBAAuB,EAAE,MAAM,mEAAmE,CAAC;AAC5G,OAAO,EAAE,2BAA2B,EAAE,MAAM,wEAAwE,CAAC;AACrH,OAAO,EAAE,qBAAqB,EAAE,MAAM,iEAAiE,CAAC;AACxG,OAAO,EAAE,yBAAyB,EAAE,MAAM,sEAAsE,CAAC;AACjH,OAAO,EAAE,8BAA8B,EAAE,MAAM,2EAA2E,CAAC;AAC3H,OAAO,EAAE,4BAA4B,EAAE,MAAM,yEAAyE,CAAC;AACvH,OAAO,EAAE,uBAAuB,EAAE,MAAM,oEAAoE,CAAC;AAC7G,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAElE,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,wBAAwB;IAC9B,KAAK,EAAE,wBAAwB;IAC/B,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE;QACL,sBAAsB;QACtB,4BAA4B;QAC5B,uBAAuB;QACvB,yBAAyB;QACzB,qBAAqB;QACrB,uBAAuB;QACvB,2BAA2B;QAC3B,8BAA8B;KAC/B;IACD,SAAS,EAAE,CAAC,yBAAyB,CAAC;IACtC,OAAO,EAAE,EAAE;IACX,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE;QACV,YAAY,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE;QACzD,gBAAgB,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE;QAC7D,0BAA0B,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE;KACxE;IACD,YAAY,EAAE,4yBAA4yB;IAE1zB,KAAK,CAAC,IAAI;QACR,MAAM,YAAY,GAAG,eAAe,EAAE,CAAC;QACvC,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QAC1D,eAAe,EAAE,CAAC;IACpB,CAAC;CACF,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"noaa-marine-find-stations.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/noaa-marine-find-stations.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAiLjE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgkBjC,CAAC"}
1
+ {"version":3,"file":"noaa-marine-find-stations.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/noaa-marine-find-stations.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAkLjE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqkBjC,CAAC"}