@cyanheads/pubmed-mcp-server 2.10.11 → 2.10.13

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 (40) hide show
  1. package/AGENTS.md +19 -18
  2. package/CLAUDE.md +19 -18
  3. package/README.md +87 -119
  4. package/dist/config/server-config.d.ts +4 -4
  5. package/dist/config/server-config.d.ts.map +1 -1
  6. package/dist/config/server-config.js +9 -24
  7. package/dist/config/server-config.js.map +1 -1
  8. package/dist/mcp-server/resources/definitions/database-info.resource.js +6 -6
  9. package/dist/mcp-server/resources/definitions/database-info.resource.js.map +1 -1
  10. package/dist/services/ncbi/formatting/citation-formatter.d.ts +13 -0
  11. package/dist/services/ncbi/formatting/citation-formatter.d.ts.map +1 -1
  12. package/dist/services/ncbi/formatting/citation-formatter.js +35 -11
  13. package/dist/services/ncbi/formatting/citation-formatter.js.map +1 -1
  14. package/dist/services/ncbi/ncbi-service.d.ts.map +1 -1
  15. package/dist/services/ncbi/ncbi-service.js +7 -4
  16. package/dist/services/ncbi/ncbi-service.js.map +1 -1
  17. package/dist/services/ncbi/parsing/article-parser.d.ts.map +1 -1
  18. package/dist/services/ncbi/parsing/article-parser.js +14 -22
  19. package/dist/services/ncbi/parsing/article-parser.js.map +1 -1
  20. package/dist/services/ncbi/parsing/esummary-parser.d.ts +11 -1
  21. package/dist/services/ncbi/parsing/esummary-parser.d.ts.map +1 -1
  22. package/dist/services/ncbi/parsing/esummary-parser.js +16 -7
  23. package/dist/services/ncbi/parsing/esummary-parser.js.map +1 -1
  24. package/dist/services/ncbi/parsing/pmc-article-parser.d.ts.map +1 -1
  25. package/dist/services/ncbi/parsing/pmc-article-parser.js +26 -3
  26. package/dist/services/ncbi/parsing/pmc-article-parser.js.map +1 -1
  27. package/dist/services/ncbi/parsing/pmc-xml-helpers.d.ts +19 -0
  28. package/dist/services/ncbi/parsing/pmc-xml-helpers.d.ts.map +1 -1
  29. package/dist/services/ncbi/parsing/pmc-xml-helpers.js +81 -2
  30. package/dist/services/ncbi/parsing/pmc-xml-helpers.js.map +1 -1
  31. package/dist/services/ncbi/parsing/xml-helpers.d.ts +29 -3
  32. package/dist/services/ncbi/parsing/xml-helpers.d.ts.map +1 -1
  33. package/dist/services/ncbi/parsing/xml-helpers.js +46 -0
  34. package/dist/services/ncbi/parsing/xml-helpers.js.map +1 -1
  35. package/dist/services/ncbi/response-handler.d.ts +9 -0
  36. package/dist/services/ncbi/response-handler.d.ts.map +1 -1
  37. package/dist/services/ncbi/response-handler.js +91 -32
  38. package/dist/services/ncbi/response-handler.js.map +1 -1
  39. package/package.json +9 -7
  40. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** @cyanheads/pubmed-mcp-server
4
- **Version:** 2.10.11
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.8`
6
- **Engines:** Bun ≥1.3.0, Node ≥24.0.0
4
+ **Version:** 2.10.13
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.0`
6
+ **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
 
8
8
  > **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.
9
9
 
@@ -111,12 +111,10 @@ export const databaseInfoResource = resource('pubmed://database/info', {
111
111
  import { z } from '@cyanheads/mcp-ts-core';
112
112
  import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
113
113
 
114
- const emptyAsUndefined = (v: unknown) => (v === '' ? undefined : v);
115
-
116
114
  const ServerConfigSchema = z.object({
117
- apiKey: z.preprocess(emptyAsUndefined, z.string().optional()).describe('NCBI API key'),
115
+ apiKey: z.string().optional().describe('NCBI API key'),
118
116
  toolIdentifier: z.string().default('pubmed-mcp-server').describe('NCBI tool identifier'),
119
- adminEmail: z.preprocess(emptyAsUndefined, z.email().optional()).describe('Admin contact email'),
117
+ adminEmail: z.email().optional().describe('Admin contact email'),
120
118
  requestDelayMs: z.coerce.number().min(50).max(5000).default(334).describe('Request delay in ms'),
121
119
  maxRetries: z.coerce.number().min(0).max(10).default(6).describe('Max retry attempts'),
122
120
  timeoutMs: z.coerce.number().min(1000).max(120000).default(30000).describe('Request timeout in ms'),
@@ -138,6 +136,8 @@ export function getServerConfig(): z.infer<typeof ServerConfigSchema> {
138
136
 
139
137
  `parseEnvConfig` maps Zod schema paths → env var names so validation errors name the actual variable (`NCBI_REQUEST_DELAY_MS` must be a number) rather than the internal path (`requestDelayMs: expected number`).
140
138
 
139
+ An empty value and a whole-value `${…}` placeholder (what an MCPB or plugin host forwards when a user leaves an option blank) read as unset: an optional field stays `undefined`, a defaulted field takes its default. No per-field `z.preprocess` guard is needed.
140
+
141
141
  For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
142
142
 
143
143
  ### Server identity and instructions
@@ -267,9 +267,9 @@ src/
267
267
 
268
268
  ## Skills
269
269
 
270
- Skills are modular instructions in `skills/` at the project root. Read them directly when a task matches — e.g., `skills/add-tool/SKILL.md` when adding a tool.
270
+ 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.
271
271
 
272
- **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). This makes skills available as context without needing to reference `skills/` paths manually. After framework updates, run the `maintenance` skill — it re-syncs the agent directory automatically (Phase B).
272
+ **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.
273
273
 
274
274
  Available skills:
275
275
 
@@ -284,7 +284,7 @@ Available skills:
284
284
  | `add-service` | Scaffold a new service integration |
285
285
  | `add-test` | Scaffold test file for a tool, resource, or service |
286
286
  | `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
287
- | `tool-defs-analysis` | Read-only audit of definition language: voice, leaks, defaults, recovery hints, examples |
287
+ | `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
288
288
  | `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
289
289
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
290
290
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
@@ -309,7 +309,7 @@ Available skills:
309
309
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
310
310
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
311
311
 
312
- **Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.
312
+ **Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `framework-skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.
313
313
 
314
314
  When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
315
315
 
@@ -323,7 +323,8 @@ When you complete a skill's checklist, check the boxes and add a completion time
323
323
  | `bun run rebuild` | Clean + build |
324
324
  | `bun run clean` | Remove build artifacts |
325
325
  | `bun run devcheck` | Lint + format + typecheck + security + packaging alignment |
326
- | `bun run audit:refresh` | Delete `bun.lock`, reinstall, and re-run `bun audit`. Use when `devcheck` flags a transitive advisory — `bun update` is sticky on transitive resolutions, so the advisory may be a stale-lockfile false positive. If it survives the refresh, it's real. |
326
+ | `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` |
327
+ | `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` |
327
328
  | `bun run tree` | Generate directory structure doc |
328
329
  | `bun run format` | Auto-fix formatting (safe fixes only) |
329
330
  | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
@@ -332,7 +333,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
332
333
  | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
333
334
  | `bun run lint:mcp` | Validate MCP definitions against spec |
334
335
  | `bun run lint:packaging` | Validate env var alignment between `manifest.json` and `server.json` (skipped cleanly when `manifest.json` is absent) |
335
- | `bun run list-skills` | List skills in `skills/` with name + description |
336
+ | `bun run list-skills` | List skills in `framework-skills/` with name + description |
336
337
  | `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
337
338
  | `bun run release:github` | Create GitHub Release from an annotated tag — enforces `v<VERSION>: <subject>` title, attaches `.mcpb` bundle |
338
339
  | `bun run start:stdio` | Production mode (stdio) |
@@ -342,9 +343,9 @@ When you complete a skill's checklist, check the boxes and add a completion time
342
343
 
343
344
  ## Bundling
344
345
 
345
- `bun run bundle` produces `dist/pubmed-mcp-server.mcpb` 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 (`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 Docker deployments are unaffected. The `release-and-publish` skill attaches the bundle to the GitHub Release at a stable `releases/latest/download/pubmed-mcp-server.mcpb` URL that powers the README install badge.
346
+ `bun run bundle` produces `dist/pubmed-mcp-server.mcpb` 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 Docker deployments are unaffected. The `release-and-publish` skill attaches the bundle to the GitHub Release at a stable `releases/latest/download/pubmed-mcp-server.mcpb` URL that powers the README install badge.
346
347
 
347
- **Adding an env var requires both files**: `server.json` stdio `environmentVariables[]` (registry discovery) and `manifest.json` `mcp_config.env` (bundle install UX, plus `user_config` if user-prompted). `bun run lint:packaging` (run by `devcheck`) verifies the env var names align.
348
+ **Adding an env var requires both files**: `server.json` stdio `environmentVariables[]` (registry discovery) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `bun run 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": ""`. A user-supplied variable also goes into the plugin manifests — `.claude-plugin/plugin.json` `userConfig` + `env`, `.codex-plugin/mcp.json` `env_vars` (see Checklist).
348
349
 
349
350
  ---
350
351
 
@@ -407,7 +408,7 @@ import { getNcbiService } from '@/services/ncbi/ncbi-service.js';
407
408
  - [ ] NCBI wrapping: tests include at least one sparse payload case with omitted upstream fields
408
409
  - [ ] Registered in `createApp()` arrays (directly or via barrel exports)
409
410
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
410
- - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = package name; `interface.shortDescription` from `package.json` description
411
- - [ ] `.codex-plugin/mcp.json` updated — server name key matches `package.json` name; env vars added for any required API keys
412
- - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry with server name key, env vars for any required API keys
411
+ - [ ] `.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
412
+ - [ ] `.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
413
+ - [ ] `.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`
413
414
  - [ ] `bun run devcheck` passes
package/CLAUDE.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** @cyanheads/pubmed-mcp-server
4
- **Version:** 2.10.11
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.8`
6
- **Engines:** Bun ≥1.3.0, Node ≥24.0.0
4
+ **Version:** 2.10.13
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.0`
6
+ **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
 
8
8
  > **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.
9
9
 
@@ -111,12 +111,10 @@ export const databaseInfoResource = resource('pubmed://database/info', {
111
111
  import { z } from '@cyanheads/mcp-ts-core';
112
112
  import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
113
113
 
114
- const emptyAsUndefined = (v: unknown) => (v === '' ? undefined : v);
115
-
116
114
  const ServerConfigSchema = z.object({
117
- apiKey: z.preprocess(emptyAsUndefined, z.string().optional()).describe('NCBI API key'),
115
+ apiKey: z.string().optional().describe('NCBI API key'),
118
116
  toolIdentifier: z.string().default('pubmed-mcp-server').describe('NCBI tool identifier'),
119
- adminEmail: z.preprocess(emptyAsUndefined, z.email().optional()).describe('Admin contact email'),
117
+ adminEmail: z.email().optional().describe('Admin contact email'),
120
118
  requestDelayMs: z.coerce.number().min(50).max(5000).default(334).describe('Request delay in ms'),
121
119
  maxRetries: z.coerce.number().min(0).max(10).default(6).describe('Max retry attempts'),
122
120
  timeoutMs: z.coerce.number().min(1000).max(120000).default(30000).describe('Request timeout in ms'),
@@ -138,6 +136,8 @@ export function getServerConfig(): z.infer<typeof ServerConfigSchema> {
138
136
 
139
137
  `parseEnvConfig` maps Zod schema paths → env var names so validation errors name the actual variable (`NCBI_REQUEST_DELAY_MS` must be a number) rather than the internal path (`requestDelayMs: expected number`).
140
138
 
139
+ An empty value and a whole-value `${…}` placeholder (what an MCPB or plugin host forwards when a user leaves an option blank) read as unset: an optional field stays `undefined`, a defaulted field takes its default. No per-field `z.preprocess` guard is needed.
140
+
141
141
  For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
142
142
 
143
143
  ### Server identity and instructions
@@ -267,9 +267,9 @@ src/
267
267
 
268
268
  ## Skills
269
269
 
270
- Skills are modular instructions in `skills/` at the project root. Read them directly when a task matches — e.g., `skills/add-tool/SKILL.md` when adding a tool.
270
+ 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.
271
271
 
272
- **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). This makes skills available as context without needing to reference `skills/` paths manually. After framework updates, run the `maintenance` skill — it re-syncs the agent directory automatically (Phase B).
272
+ **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.
273
273
 
274
274
  Available skills:
275
275
 
@@ -284,7 +284,7 @@ Available skills:
284
284
  | `add-service` | Scaffold a new service integration |
285
285
  | `add-test` | Scaffold test file for a tool, resource, or service |
286
286
  | `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
287
- | `tool-defs-analysis` | Read-only audit of definition language: voice, leaks, defaults, recovery hints, examples |
287
+ | `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
288
288
  | `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
289
289
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
290
290
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
@@ -309,7 +309,7 @@ Available skills:
309
309
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
310
310
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
311
311
 
312
- **Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.
312
+ **Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `framework-skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.
313
313
 
314
314
  When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
315
315
 
@@ -323,7 +323,8 @@ When you complete a skill's checklist, check the boxes and add a completion time
323
323
  | `bun run rebuild` | Clean + build |
324
324
  | `bun run clean` | Remove build artifacts |
325
325
  | `bun run devcheck` | Lint + format + typecheck + security + packaging alignment |
326
- | `bun run audit:refresh` | Delete `bun.lock`, reinstall, and re-run `bun audit`. Use when `devcheck` flags a transitive advisory — `bun update` is sticky on transitive resolutions, so the advisory may be a stale-lockfile false positive. If it survives the refresh, it's real. |
326
+ | `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` |
327
+ | `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` |
327
328
  | `bun run tree` | Generate directory structure doc |
328
329
  | `bun run format` | Auto-fix formatting (safe fixes only) |
329
330
  | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
@@ -332,7 +333,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
332
333
  | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
333
334
  | `bun run lint:mcp` | Validate MCP definitions against spec |
334
335
  | `bun run lint:packaging` | Validate env var alignment between `manifest.json` and `server.json` (skipped cleanly when `manifest.json` is absent) |
335
- | `bun run list-skills` | List skills in `skills/` with name + description |
336
+ | `bun run list-skills` | List skills in `framework-skills/` with name + description |
336
337
  | `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
337
338
  | `bun run release:github` | Create GitHub Release from an annotated tag — enforces `v<VERSION>: <subject>` title, attaches `.mcpb` bundle |
338
339
  | `bun run start:stdio` | Production mode (stdio) |
@@ -342,9 +343,9 @@ When you complete a skill's checklist, check the boxes and add a completion time
342
343
 
343
344
  ## Bundling
344
345
 
345
- `bun run bundle` produces `dist/pubmed-mcp-server.mcpb` 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 (`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 Docker deployments are unaffected. The `release-and-publish` skill attaches the bundle to the GitHub Release at a stable `releases/latest/download/pubmed-mcp-server.mcpb` URL that powers the README install badge.
346
+ `bun run bundle` produces `dist/pubmed-mcp-server.mcpb` 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 Docker deployments are unaffected. The `release-and-publish` skill attaches the bundle to the GitHub Release at a stable `releases/latest/download/pubmed-mcp-server.mcpb` URL that powers the README install badge.
346
347
 
347
- **Adding an env var requires both files**: `server.json` stdio `environmentVariables[]` (registry discovery) and `manifest.json` `mcp_config.env` (bundle install UX, plus `user_config` if user-prompted). `bun run lint:packaging` (run by `devcheck`) verifies the env var names align.
348
+ **Adding an env var requires both files**: `server.json` stdio `environmentVariables[]` (registry discovery) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `bun run 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": ""`. A user-supplied variable also goes into the plugin manifests — `.claude-plugin/plugin.json` `userConfig` + `env`, `.codex-plugin/mcp.json` `env_vars` (see Checklist).
348
349
 
349
350
  ---
350
351
 
@@ -407,7 +408,7 @@ import { getNcbiService } from '@/services/ncbi/ncbi-service.js';
407
408
  - [ ] NCBI wrapping: tests include at least one sparse payload case with omitted upstream fields
408
409
  - [ ] Registered in `createApp()` arrays (directly or via barrel exports)
409
410
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
410
- - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = package name; `interface.shortDescription` from `package.json` description
411
- - [ ] `.codex-plugin/mcp.json` updated — server name key matches `package.json` name; env vars added for any required API keys
412
- - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry with server name key, env vars for any required API keys
411
+ - [ ] `.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
412
+ - [ ] `.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
413
+ - [ ] `.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`
413
414
  - [ ] `bun run devcheck` passes
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
 
11
11
 
12
- [![Version](https://img.shields.io/badge/Version-2.10.11-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/pubmed-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/pubmed-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/pubmed-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.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
12
+ [![Version](https://img.shields.io/badge/Version-2.10.13-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/pubmed-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/pubmed-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/pubmed-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.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
13
13
 
14
14
  </div>
15
15
 
@@ -29,9 +29,11 @@
29
29
 
30
30
  ---
31
31
 
32
- ## Tools
32
+ ## Overview
33
33
 
34
- 11 tools for working with PubMed, PubMed Central, and Europe PMC data:
34
+ An MCP server over NCBI's E-utilities, PubMed Central, and Europe PMC. Search the biomedical literature, fetch metadata and full text, resolve identifiers and partial citations, format references, and ground queries in MeSH vocabulary. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
35
+
36
+ ### Tools
35
37
 
36
38
  | Tool | Description |
37
39
  |:---|:---|
@@ -42,173 +44,139 @@
42
44
  | `pubmed_fetch_fulltext` | Fetch full-text articles via a chain: NCBI PMC EFetch → Europe PMC `fullTextXML` → Unpaywall. Accepts PMIDs, PMCIDs, or DOIs. |
43
45
  | `pubmed_format_citations` | Generate formatted citations in APA 7th, MLA 9th, BibTeX, RIS, or Vancouver (ICMJE/NLM) |
44
46
  | `pubmed_find_related` | Find similar articles, citing articles, or references for a given PMID |
45
- | `pubmed_spell_check` | Spell-check biomedical queries using NCBI's ESpell service |
46
- | `pubmed_lookup_mesh` | Search and explore MeSH vocabulary — tree numbers, scope notes, entry terms |
47
+ | `pubmed_spell_check` | Spell-check a biomedical query via NCBI ESpell — returns the corrected query and whether a suggestion was found |
48
+ | `pubmed_lookup_mesh` | Search MeSH by heading — tree numbers, scope notes, entry terms — for building controlled-vocabulary queries |
47
49
  | `pubmed_lookup_citation` | Resolve partial bibliographic references to PubMed IDs via ECitMatch |
48
50
  | `pubmed_convert_ids` | Convert between DOI, PMID, and PMCID using the PMC ID Converter API |
49
51
 
50
- ### `pubmed_search_articles`
52
+ ### Resources
51
53
 
52
- Search PubMed with full NCBI query syntax and filters.
54
+ | Resource | Description |
55
+ |:---|:---|
56
+ | `pubmed://database/info` | PubMed database metadata via EInfo (field list, record count, last update) |
53
57
 
54
- - Free-text queries with PubMed's full boolean and field-tag syntax
55
- - Field-specific filters: author, journal, MeSH terms, language, species
56
- - Common filters: has abstract, free full text
57
- - Date range filtering by publication, modification, or Entrez date
58
- - Publication type filtering (Review, Clinical Trial, Meta-Analysis, etc.)
59
- - Sort by relevance, publication date, author, or journal
60
- - Pagination via offset for paging through large result sets
61
- - Optional brief summaries for top N results via ESummary
62
- - NCBI Bookshelf results carry their own venue — `bookTitle`, `publisherName`, and `docType` (`chapter`, `book`, or `citation`) — because PubMed leaves `source` empty on them; the book's editors are reported in `editors`, apart from the chapter's own authors
63
- - Returns the original query plus the fully applied PubMed query and normalized filter metadata
58
+ ### Prompts
64
59
 
65
- ---
60
+ | Prompt | Description |
61
+ |:---|:---|
62
+ | `research_plan` | Generate a structured 4-phase biomedical research plan outline |
66
63
 
67
- ### `pubmed_fetch_articles`
64
+ ## Capability reference
68
65
 
69
- Fetch full article metadata by PubMed IDs.
66
+ ### `pubmed_search_articles` <sub>tool</sub>
70
67
 
71
- - Batch fetch up to 200 articles at once (auto-switches to POST for batches >= 100)
72
- - Returns structured data: title, abstract, authors with deduplicated affiliations, journal info, DOI
73
- - Direct links to PubMed and PubMed Central (when available)
74
- - Optional MeSH terms, grant information, and publication types
75
- - Handles PubMed's inconsistent XML (structured abstracts, missing fields, varying date formats)
76
- - NCBI Bookshelf chapters and whole books are returned as first-class records, not reported unavailable: `recordType` (`journal-article`, `book-chapter`, `book`) tells them apart, and a `book` object carries the book title, publisher, place, dates, medium, edition, series, ISBNs, book DOI, editors, and Bookshelf accession. `journalInfo` is absent on those records — a book title is never reported as a journal
77
- - Journals that assign article numbers instead of page ranges often carry no pagination at all; the number is reported as `journalInfo.elocationId` with its `journalInfo.elocationIdType` (`pii`), never merged into `journalInfo.pages` and never confused with the DOI
78
- - Opt-in whole-response ceiling: `maxResponseCharacters` keeps complete article records in response order until the next one would cross it, then defers the rest whole and lists their PMIDs in `deferred.ids`. Re-call with those PMIDs to resume exactly where the response stopped — no article is split, skipped, or duplicated. Each article is measured as the JSON record it is returned as, so a ceiling under the first article returns zero articles, the full deferred list, and the size to clear
68
+ - Full PubMed boolean and field-tag syntax, plus structured filters: author, journal, MeSH terms, language, species, publication type, has-abstract, free-full-text
69
+ - Date ranges by publication, modification, or Entrez date; sort by relevance, date, author, or journal; offset pagination
70
+ - Optional brief summaries for the top N results via ESummary
71
+ - NCBI Bookshelf hits carry `bookTitle`, `publisherName`, `docType`, and `editors` in place of the empty `source`
72
+ - Echoes the original query, the fully applied PubMed query, and normalized filter metadata
79
73
 
80
74
  ---
81
75
 
82
- ### `pubmed_fetch_fulltext`
83
-
84
- Fetch full-text articles via a three-stage chain: NCBI PMC EFetch → Europe PMC `fullTextXML` → Unpaywall.
85
-
86
- - Accepts exactly one of `pmcids` (direct PMC IDs), `pmids` (PubMed IDs, auto-resolved), or `dois` (auto-resolved to PMC via the ID Converter; preprints and EPMC-only OA fall through to Europe PMC / Unpaywall). One identifier per element in every branch — a DOI carrying a comma or whitespace is rejected at the schema
87
- - NCBI PMC and Europe PMC both return structured JATS; output records origin via `viaSource: "pmc" | "europepmc" | "unpaywall"`
88
- - Europe PMC layer (enabled by default; disable with `EUROPEPMC_ENABLED=false`) recovers PMC-counterpart records that NCBI PMC EFetch missed, and resolves DOI input to PMC counterparts when one exists. EPMC's `fullTextXML` is PMC-keyed, so preprints (PPR), patents (PAT), and Agricola (AGR) are reachable via `pubmed_europepmc_search` for metadata but have no full text via this chain.
89
- - Unpaywall layer (enabled by setting `UNPAYWALL_EMAIL`) resolves DOIs to legal OA copies; extracts HTML landing pages to Markdown via Defuddle or PDFs to text via unpdf
90
- - Discriminated output contract — `source: "pmc"` (structured sections, regardless of whether it came from PMC or EPMC) or `source: "unpaywall"` (best-effort body + `contentFormat`: `html-markdown` or `pdf-text`)
91
- - Structured unavailable reasons (`not-found`, `no-pmc-fallback-disabled`, `no-epmc-fulltext`, `no-body`, `no-doi`, `doi-lookup-failed`, `no-oa`, `fetch-failed`, `parse-failed`, `service-error`) so callers can retry or explain to users without parsing text. `no-doi` and `doi-lookup-failed` are the settled and unsettled halves of the same gap: the first means the DOI lookup ran and the record has none, the second that the lookup itself errored, so a DOI may well exist and the request is worth retrying
92
- - An `unavailable` entry also carries `unqueriedTiers` when the chain skipped a tier this deployment has not configured and that tier could have served the id — the search was incomplete, and a deployment with those tiers configured may still resolve it
93
- - Each `unavailable` entry carries `idType` (`pmid` / `pmcid` / `doi`) and `triedTiers` — per-tier outcomes (`not-attempted`, `miss`, `no-fulltext`, `service-error`, …) in execution order, so callers can see which stage failed and why
94
- - Section filtering by title (case-insensitive substring match at any nesting depth, e.g. `["methods", "results"]`) and configurable max sections apply to PMC output. A section that matches directly is returned whole; one kept only because a nested subsection matched keeps its heading as a breadcrumb with its own text cleared
95
- - Tables are returned as structured cells (`tables[]` on each PMC article — rows, caption, label, footnotes, and the enclosing section, named for back-matter and appendix tables as well as body ones), covering `<floats-group>`, `<back>` and appendix deposits alongside body tables. `colspan` and `rowspan` are expanded to one entry per grid column, so a value stays under the header it belongs to on both output surfaces; a cell spanning several columns or rows repeats across the cells it covers. A deposit with no readable markup comes back labelled with an `unextractableReason` rather than silently missing. Turn them off with `includeTables: false`
96
- - Figures and supplementary material come back as structured entries (`assets[]` on each PMC article — `assetType`, label, caption, the enclosing section, and the `<graphic>`/`<media>` pointer exactly as deposited, which is a name inside the PMC deposit rather than a fetchable URL), covering `<floats-group>`, `<back>` and appendix placements alongside body ones. Each one lifted out of the body leaves a `[Figure: <label>]` / `[Supplementary: <label>]` marker at its position, so reading order survives the lift. Turn them off with `includeAssets: false`, which removes the markers with them. Prose-shaped blocks — lists, definition lists, block quotes, boxed text, preformatted blocks, displayed formulae — render into the section text at their document position instead, and no block is ever concatenated into a neighbouring sentence
97
- - Character budgets keep context size predictable: `maxCharacters` caps body text per article (PMC sections and subsections, inline blocks included, plus table content — cell, caption, label and footnote text, not the Markdown grid rendered around it — and asset label, caption and pointer text; or the Unpaywall body), `maxCharactersPerSection` caps a single PMC section, and `overflowMode` picks between `truncate` (fill sections in document order) and `outline` (split the budget evenly so every heading survives with an excerpt). Sections are served first, then tables, then assets, each spending what is left in document order until one does not fit; that entry and the rest are dropped whole rather than cut mid-row or returned with a shortened caption, and named in `truncation.articles[].omittedTableNames` / `omittedAssetNames`. Budgets run after the semantic filters, and a `truncation` object reports per-article and per-section character counts whenever anything was shortened
98
- - `maxResponseCharacters` bounds the whole response instead of each body: every field of a returned record counts (abstract, references, metadata, body), one ledger across PMC-, Europe PMC-, and Unpaywall-served articles. Articles past the ceiling are deferred whole, with their ids — in the branch they were requested under — in `deferred.ids` for a follow-up call
99
- - Up to 10 articles per request
76
+ ### `pubmed_fetch_articles` <sub>tool</sub>
100
77
 
101
- ---
78
+ - Up to 200 PMIDs per call (POST for batches of 100 or more)
79
+ - Title, abstract, authors with deduplicated affiliations, journal info, DOI, PubMed/PMC links; optional MeSH terms, grants, and publication types
80
+ - Tolerant of PubMed's inconsistent XML — structured abstracts, missing fields, varying date formats
81
+ - Bookshelf chapters and books are first-class: `recordType` (`journal-article` / `book-chapter` / `book`) plus a `book` object (title, publisher, editors, ISBNs, Bookshelf accession); `journalInfo` is absent on them
82
+ - Article-number journals report `journalInfo.elocationId` + `elocationIdType` rather than a page range
83
+ - Opt-in `maxResponseCharacters` keeps whole records in order until the ceiling, then defers the rest to `deferred.ids` for a follow-up call
102
84
 
103
- ### `pubmed_europepmc_search`
85
+ ---
104
86
 
105
- Search Europe PMC (EBI/EMBL-EBI), a broader open-access biomedical corpus than PubMed alone.
87
+ ### `pubmed_fetch_fulltext` <sub>tool</sub>
106
88
 
107
- - Surfaces records PubMed search can't reach — preprints (`source: PPR`), patents (`source: PAT`), Agricola (`source: AGR`), plus everything in PubMed (`MED`) and PMC (`PMC`). On recent queries this can mean dozens of relevant hits with zero PubMed overlap.
108
- - Default sources `["MED", "PMC", "PPR"]`; pass `sources` to include `PAT` / `AGR`
109
- - Cursor-based pagination via `cursorMark` (unlike `pubmed_search_articles`, which uses offset) — `*` for the first page, return `nextCursorMark` for the next
110
- - Output discriminator on `source` plus optional `pmid` / `pmcId` / `doi` cross-walking
111
- - `abstractSnippet` is capped at 400 characters to keep a page bounded; `abstractTruncated` says whether it was cut, and `pubmed_europepmc_fetch` returns the whole abstract for the records worth reading in full
112
- - Disabled when `EUROPEPMC_ENABLED=false`; tool is not registered in that case
89
+ - Exactly one of `pmcids`, `pmids`, or `dois` (one id per element), up to 10 per request
90
+ - Three-tier chain: NCBI PMC EFetch → Europe PMC `fullTextXML` (`EUROPEPMC_ENABLED`, default on) → Unpaywall (needs `UNPAYWALL_EMAIL`); `viaSource` names which tier served each article
91
+ - Preprints, patents, and Agricola records have metadata via `pubmed_europepmc_search` but no full text through this chain — Europe PMC's `fullTextXML` is PMC-keyed
92
+ - `source: "pmc"` returns structured sections plus `tables[]` (cells, caption, label, footnotes) and `assets[]` (figures and supplementary material, with `[Figure: <label>]` markers left in the body); `source: "unpaywall"` returns a best-effort body with `contentFormat` (`html-markdown` / `pdf-text`)
93
+ - Unavailable entries carry a typed `reason` (`not-found`, `no-doi`, `doi-lookup-failed`, `no-oa`, `service-error`, …), `idType`, `triedTiers` (per-tier outcome in execution order), and `unqueriedTiers` when an unconfigured tier could have served the id
94
+ - Filters and budgets: `sections` (case-insensitive title match), `maxSections`, `includeTables`, `includeAssets`, `maxCharacters`, `maxCharactersPerSection`, `overflowMode` (`truncate` / `outline`), and `maxResponseCharacters`, which defers whole articles past the ceiling to `deferred.ids`; a `truncation` object reports what was shortened or omitted
113
95
 
114
96
  ---
115
97
 
116
- ### `pubmed_europepmc_fetch`
117
-
118
- Fetch complete Europe PMC records by `source` + `epmcId`, the detail counterpart to `pubmed_europepmc_search`.
98
+ ### `pubmed_europepmc_search` <sub>tool</sub>
119
99
 
120
- - Returns the full, untruncated abstract as display-ready plain text — markup stripped, HTML entities decoded
121
- - Addressed by the `source` and `epmcId` of a search hit, the only identifier preprint (`PPR`), patent (`PAT`), and Agricola (`AGR`) records reliably carry — `pubmed_fetch_articles` needs a PMID and `pubmed_fetch_fulltext` needs a PMCID, PMID, or DOI
122
- - Up to 25 records per call, resolved in a single Europe PMC request
123
- - Pairs unresolved requests back to the caller in `notFound` instead of failing the batch
124
- - Disabled when `EUROPEPMC_ENABLED=false`; tool is not registered in that case
100
+ - Reaches records PubMed can't: preprints (`PPR`), patents (`PAT`), Agricola (`AGR`), alongside `MED` and `PMC`; default `sources` is `["MED", "PMC", "PPR"]`
101
+ - Cursor pagination via `cursorMark` — `*` for the first page, then `nextCursorMark`
102
+ - Hits carry `source` plus `pmid` / `pmcId` / `doi` when known; `abstractSnippet` is capped at 400 characters, with `abstractTruncated` flagging the cut
103
+ - Not registered when `EUROPEPMC_ENABLED=false`
125
104
 
126
105
  ---
127
106
 
128
- ### `pubmed_format_citations`
129
-
130
- Generate formatted citations for articles.
107
+ ### `pubmed_europepmc_fetch` <sub>tool</sub>
131
108
 
132
- - Five citation styles: APA 7th, MLA 9th, BibTeX, RIS, Vancouver (ICMJE/NLM)
133
- - NCBI Bookshelf chapters and whole books cite in their own form in every style — Vancouver's `In: … editors` contribution pattern, APA's chapter-in-edited-book, MLA's `edited by`, BibTeX `@incollection` / `@book`, RIS `CHAP` / `BOOK` — carrying the book title, editors, publisher, place, ISBNs and Bookshelf URL
134
- - An article with no page range cites by its electronic article locator in each style's own convention — Vancouver's trailing `pii:` note, APA's `Article <n>`, MLA's `art. <n>`, biblatex `eid`, RIS `C7` — rather than dropping it or writing it into a page field
135
- - Request multiple styles per article in a single call
136
- - Hand-rolled formatters — zero external dependencies, fully Workers-compatible
137
- - Up to 50 articles per request
138
- - Reports formatted counts and unavailable PMIDs for partial-result handling
109
+ - Full records with the untruncated plain-text abstract, addressed by `source` + `epmcId` — the only identifier preprint, patent, and Agricola records reliably carry
110
+ - Up to 25 per call in one Europe PMC request; unresolved ids come back in `notFound` rather than failing the batch
111
+ - Not registered when `EUROPEPMC_ENABLED=false`
139
112
 
140
113
  ---
141
114
 
142
- ### `pubmed_find_related`
115
+ ### `pubmed_format_citations` <sub>tool</sub>
143
116
 
144
- Find articles related to a source article via ELink.
145
-
146
- - Three relationship types: `similar` (content similarity), `cited_by`, `references`
147
- - Results enriched with title, authors, publication date, and source via ESummary — or, for an NCBI Bookshelf record, its book title, publisher, and doc type in place of the empty source
148
- - Results returned in NCBI's relevance order
149
- - Falls back to Europe PMC, then OpenAlex, when NCBI cannot answer; the response names which provider served it. A request no provider can answer fails with a typed `all_providers_failed` error instead of an empty result
117
+ - APA 7th, MLA 9th, BibTeX, RIS, Vancouver (ICMJE/NLM); several styles per article in one call, up to 50 articles
118
+ - Bookshelf chapters and books cite in each style's edited-book form; articles without a page range cite by electronic locator in each style's convention
119
+ - Hand-rolled formatters — zero dependencies, Workers-compatible
120
+ - Reports formatted counts and unavailable PMIDs
150
121
 
151
122
  ---
152
123
 
153
- ### `pubmed_spell_check`
154
-
155
- Spell-check a biomedical query using NCBI's ESpell.
124
+ ### `pubmed_find_related` <sub>tool</sub>
156
125
 
157
- - Returns the original query, corrected query, and whether a suggestion was found
158
- - Useful for query refinement before searching
126
+ - `similar`, `cited_by`, or `references` for a PMID, in NCBI relevance order, enriched with title, authors, date, and source (or Bookshelf book title and publisher)
127
+ - Falls back to Europe PMC, then OpenAlex, when NCBI can't answer; the response names the provider. Fails with a typed `all_providers_failed` error rather than an empty result
159
128
 
160
129
  ---
161
130
 
162
- ### `pubmed_lookup_mesh`
131
+ ### `pubmed_spell_check` <sub>tool</sub>
163
132
 
164
- Search and explore the MeSH (Medical Subject Headings) vocabulary.
133
+ - Runs a query through NCBI ESpell and returns `original`, `corrected`, and `hasSuggestion`
134
+ - A blank or whitespace-only query is rejected rather than sent upstream
165
135
 
166
- - Search MeSH terms by name with exact-heading matching
167
- - Detailed records with tree numbers, scope notes, and entry terms by default
168
- - Useful for building precise PubMed queries with controlled vocabulary
136
+ ---
137
+
138
+ ### `pubmed_lookup_mesh` <sub>tool</sub>
139
+
140
+ - Looks up MeSH descriptors by name or free-text term, pinning the exact-heading match to the top of the first page
141
+ - Records carry `meshId` (DescriptorUI), `entrezUid`, and, with `includeDetails` (default on), tree numbers, scope notes, and entry terms
142
+ - `maxResults` up to 50 with offset pagination via `nextOffset`; `totalCount` reports the upstream match count
169
143
 
170
144
  ---
171
145
 
172
- ### `pubmed_lookup_citation`
146
+ ### `pubmed_lookup_citation` <sub>tool</sub>
173
147
 
174
- Resolve partial bibliographic references to PubMed IDs via NCBI ECitMatch.
148
+ - Match on journal, year, volume, first page, and/or author — at least one field, more fields for better precision; up to 25 per call
149
+ - Pipes and line breaks are rejected at the schema (ECitMatch's wire format is pipe-delimited); the free-form `key` label is exempt
150
+ - Explicit `matched`, `not_found`, and `ambiguous` statuses with recovery detail
151
+
152
+ ---
175
153
 
176
- - Match citations by journal, year, volume, first page, and/or author name
177
- - More fields = better match accuracy; at least one field required
178
- - Bibliographic fields cannot contain a pipe (`|`) or a line break — ECitMatch's wire format is pipe-delimited, so those characters are rejected at the schema; the free-form `key` label is exempt
179
- - Batch up to 25 citations per request
180
- - Deterministic matching — more reliable than free-text search for known references
181
- - Returns explicit `matched`, `not_found`, and `ambiguous` statuses with recovery detail
154
+ ### `pubmed_convert_ids` <sub>tool</sub>
155
+
156
+ - Up to 50 DOIs, PMIDs, or PMCIDs per call, all one type; only PMC-indexed articles resolve
157
+ - One id per element — a packed `"23193287,37952131"` is rejected rather than expanded
158
+ - Per-id success/error rows; a partial batch never fails as a whole
182
159
 
183
160
  ---
184
161
 
185
- ### `pubmed_convert_ids`
162
+ ### `pubmed://database/info` <sub>resource</sub>
186
163
 
187
- Convert between article identifiers (DOI, PMID, PMCID) using the PMC ID Converter API.
164
+ - Live EInfo call for the `pubmed` database, returned as `application/json`
165
+ - `dbName`, `description`, `count`, `lastUpdate`, and `fields[]` — each field's short `name` (the tag usable in `pubmed_search_articles` queries), `fullName`, and `description`
166
+ - No parameters
188
167
 
189
- - Batch up to 50 IDs per request
190
- - Accepts DOIs, PMIDs, or PMCIDs (all IDs must be the same type)
191
- - One identifier per array element, checked against `idType` before the request — a packed value like `"23193287,37952131"` is rejected rather than expanded into extra records, since a comma is the converter's list delimiter in any encoding
192
- - Only resolves articles indexed in PubMed Central
193
- - Per-ID success/error reporting — partial batches return resolved mappings alongside structured errors for unresolvable IDs, not a batch-level failure
168
+ ---
194
169
 
195
- ## Resource and prompt
170
+ ### `research_plan` <sub>prompt</sub>
196
171
 
197
- | Type | Name | Description |
198
- |:---|:---|:---|
199
- | Resource | `pubmed://database/info` | PubMed database metadata via EInfo (field list, record count, last update) |
200
- | Prompt | `research_plan` | Generate a structured 4-phase biomedical research plan outline |
172
+ - Arguments: `title`, `goal`, `keywords` (comma-separated) required; `organism` and `includeAgentPrompts` (`"true"` / `"false"`) optional
173
+ - Returns two messages: an assistant framing message (biomedical research planning assistant, grounds recommendations in the PubMed tools when available) and a user message carrying the plan
174
+ - The plan walks four phases — Conception & Planning, Data Collection & Processing, Analysis & Interpretation, Dissemination — with sub-steps under each
175
+ - `includeAgentPrompts: "true"` adds an agent-guidance block under each sub-step, several of which point at `pubmed_search_articles` and `pubmed_lookup_mesh`
201
176
 
202
177
  ## Features
203
178
 
204
- Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core):
205
-
206
- - Declarative tool definitions — single file per tool, framework handles registration and validation
207
- - Unified error handling across all tools
208
- - Pluggable auth (`none`, `jwt`, `oauth`)
209
- - Swappable storage backends: `in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`
210
- - Structured logging with optional OpenTelemetry tracing
211
- - Runs locally (stdio/HTTP) or on Cloudflare Workers from the same codebase
179
+ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports (Cloudflare Workers from the same codebase), pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
212
180
 
213
181
  PubMed-specific:
214
182
 
@@ -403,7 +371,7 @@ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rule
403
371
 
404
372
  ## Contributing
405
373
 
406
- Issues and pull requests are welcome. Run checks and tests before submitting:
374
+ Issues are welcome. Run checks and tests before submitting:
407
375
 
408
376
  ```sh
409
377
  bun run devcheck
@@ -6,18 +6,18 @@
6
6
  */
7
7
  import { z } from '@cyanheads/mcp-ts-core';
8
8
  declare const ServerConfigSchema: z.ZodObject<{
9
- apiKey: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
9
+ apiKey: z.ZodOptional<z.ZodString>;
10
10
  toolIdentifier: z.ZodDefault<z.ZodString>;
11
- adminEmail: z.ZodPreprocess<z.ZodOptional<z.ZodEmail>, unknown>;
11
+ adminEmail: z.ZodOptional<z.ZodEmail>;
12
12
  requestDelayMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
13
13
  maxConcurrent: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
14
14
  maxRetries: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
15
15
  timeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
16
16
  totalDeadlineMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
17
- unpaywallEmail: z.ZodPreprocess<z.ZodOptional<z.ZodEmail>, unknown>;
17
+ unpaywallEmail: z.ZodOptional<z.ZodEmail>;
18
18
  unpaywallTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
19
19
  europepmcEnabled: z.ZodDefault<z.ZodCodec<z.ZodString, z.ZodBoolean>>;
20
- europepmcEmail: z.ZodPreprocess<z.ZodOptional<z.ZodEmail>, unknown>;
20
+ europepmcEmail: z.ZodOptional<z.ZodEmail>;
21
21
  europepmcRequestDelayMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
22
22
  europepmcMaxRetries: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
23
23
  europepmcTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAoB3C,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;iBA4DtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CA8B9C"}
1
+ {"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;iBA8DtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CA+B9C"}