@cyanheads/orcid-mcp-server 0.1.1

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 (64) hide show
  1. package/CLAUDE.md +405 -0
  2. package/Dockerfile +98 -0
  3. package/README.md +321 -0
  4. package/changelog/0.1.x/0.1.0.md +20 -0
  5. package/changelog/0.1.x/0.1.1.md +13 -0
  6. package/changelog/template.md +119 -0
  7. package/dist/config/server-config.d.ts +12 -0
  8. package/dist/config/server-config.d.ts.map +1 -0
  9. package/dist/config/server-config.js +22 -0
  10. package/dist/config/server-config.js.map +1 -0
  11. package/dist/index.d.ts +8 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +35 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/mcp-server/resources/definitions/researcher-profile.resource.d.ts +24 -0
  16. package/dist/mcp-server/resources/definitions/researcher-profile.resource.d.ts.map +1 -0
  17. package/dist/mcp-server/resources/definitions/researcher-profile.resource.js +62 -0
  18. package/dist/mcp-server/resources/definitions/researcher-profile.resource.js.map +1 -0
  19. package/dist/mcp-server/resources/definitions/researcher-works.resource.d.ts +24 -0
  20. package/dist/mcp-server/resources/definitions/researcher-works.resource.d.ts.map +1 -0
  21. package/dist/mcp-server/resources/definitions/researcher-works.resource.js +60 -0
  22. package/dist/mcp-server/resources/definitions/researcher-works.resource.js.map +1 -0
  23. package/dist/mcp-server/tools/definitions/get-affiliations.tool.d.ts +47 -0
  24. package/dist/mcp-server/tools/definitions/get-affiliations.tool.d.ts.map +1 -0
  25. package/dist/mcp-server/tools/definitions/get-affiliations.tool.js +170 -0
  26. package/dist/mcp-server/tools/definitions/get-affiliations.tool.js.map +1 -0
  27. package/dist/mcp-server/tools/definitions/get-funding.tool.d.ts +36 -0
  28. package/dist/mcp-server/tools/definitions/get-funding.tool.d.ts.map +1 -0
  29. package/dist/mcp-server/tools/definitions/get-funding.tool.js +140 -0
  30. package/dist/mcp-server/tools/definitions/get-funding.tool.js.map +1 -0
  31. package/dist/mcp-server/tools/definitions/get-peer-reviews.tool.d.ts +35 -0
  32. package/dist/mcp-server/tools/definitions/get-peer-reviews.tool.d.ts.map +1 -0
  33. package/dist/mcp-server/tools/definitions/get-peer-reviews.tool.js +146 -0
  34. package/dist/mcp-server/tools/definitions/get-peer-reviews.tool.js.map +1 -0
  35. package/dist/mcp-server/tools/definitions/get-profile.tool.d.ts +39 -0
  36. package/dist/mcp-server/tools/definitions/get-profile.tool.d.ts.map +1 -0
  37. package/dist/mcp-server/tools/definitions/get-profile.tool.js +146 -0
  38. package/dist/mcp-server/tools/definitions/get-profile.tool.js.map +1 -0
  39. package/dist/mcp-server/tools/definitions/get-works.tool.d.ts +35 -0
  40. package/dist/mcp-server/tools/definitions/get-works.tool.d.ts.map +1 -0
  41. package/dist/mcp-server/tools/definitions/get-works.tool.js +127 -0
  42. package/dist/mcp-server/tools/definitions/get-works.tool.js.map +1 -0
  43. package/dist/mcp-server/tools/definitions/resolve-researcher.tool.d.ts +46 -0
  44. package/dist/mcp-server/tools/definitions/resolve-researcher.tool.d.ts.map +1 -0
  45. package/dist/mcp-server/tools/definitions/resolve-researcher.tool.js +250 -0
  46. package/dist/mcp-server/tools/definitions/resolve-researcher.tool.js.map +1 -0
  47. package/dist/mcp-server/tools/definitions/search-researchers.tool.d.ts +40 -0
  48. package/dist/mcp-server/tools/definitions/search-researchers.tool.d.ts.map +1 -0
  49. package/dist/mcp-server/tools/definitions/search-researchers.tool.js +192 -0
  50. package/dist/mcp-server/tools/definitions/search-researchers.tool.js.map +1 -0
  51. package/dist/services/orcid/normalizers.d.ts +32 -0
  52. package/dist/services/orcid/normalizers.d.ts.map +1 -0
  53. package/dist/services/orcid/normalizers.js +256 -0
  54. package/dist/services/orcid/normalizers.js.map +1 -0
  55. package/dist/services/orcid/orcid-service.d.ts +65 -0
  56. package/dist/services/orcid/orcid-service.d.ts.map +1 -0
  57. package/dist/services/orcid/orcid-service.js +140 -0
  58. package/dist/services/orcid/orcid-service.js.map +1 -0
  59. package/dist/services/orcid/types.d.ts +335 -0
  60. package/dist/services/orcid/types.d.ts.map +1 -0
  61. package/dist/services/orcid/types.js +6 -0
  62. package/dist/services/orcid/types.js.map +1 -0
  63. package/package.json +79 -0
  64. package/server.json +99 -0
package/README.md ADDED
@@ -0,0 +1,321 @@
1
+ <div align="center">
2
+ <h1>@cyanheads/orcid-mcp-server</h1>
3
+ <p><b>Search and retrieve researcher profiles, works, affiliations, funding, and peer review records from the ORCID registry via MCP. STDIO or Streamable HTTP.</b>
4
+ <div>7 Tools • 2 Resources</div>
5
+ </p>
6
+ </div>
7
+
8
+ <div align="center">
9
+
10
+ [![Version](https://img.shields.io/badge/Version-0.1.1-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.2-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
+
12
+ </div>
13
+
14
+ <div align="center">
15
+
16
+ [![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-67E8F9?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## Tools
23
+
24
+ Seven tools organized around three workflows — author disambiguation, researcher profiling, and cross-server identifier chaining:
25
+
26
+ | Tool | Description |
27
+ |:-----|:------------|
28
+ | `orcid_search_researchers` | Search the ORCID registry using structured field params (name, affiliation, keyword, ROR ID, DOI, PMID). All params are ANDed into a Solr query against the expanded-search endpoint, returning ORCID iDs with inline name and institution data. |
29
+ | `orcid_get_profile` | Fetch a researcher's public profile: name, biography, keywords, researcher URLs, and external identifiers (Scopus Author ID, ResearcherID, Loop, etc.). |
30
+ | `orcid_get_works` | Retrieve works (publications, datasets, software, preprints) for a researcher. Returns titles, types, publication dates, journal names, and all external identifiers — DOIs, PMIDs, arXiv IDs — ready for chaining to Crossref, PubMed, or arXiv servers. |
31
+ | `orcid_get_affiliations` | Fetch affiliation records for a researcher. Accepts a `types` list to filter which sections to return: `employment`, `education`, `invited-positions`, `distinctions`, `memberships`, `qualifications`, `services`, or `all`. |
32
+ | `orcid_get_funding` | Fetch funding records: grants, contracts, awards, and salary awards, with funder names, grant numbers, and funding periods. |
33
+ | `orcid_get_peer_reviews` | Fetch peer review activity: convening organizations, reviewer role, review type, completion dates, and ISSN-keyed group identifiers. |
34
+ | `orcid_resolve_researcher` | Disambiguate an ambiguous author name to a verified ORCID iD. Returns a ranked list of up to 5 candidates with transparent signals: name match type, institution overlap, and whether a DOI or PMID anchor was used. |
35
+
36
+ ### `orcid_search_researchers`
37
+
38
+ Search the ORCID registry with structured field parameters mapped to Solr field queries.
39
+
40
+ - Structured params — `given_name`, `family_name`, `affiliation`, `keyword`, `ror_id`, `doi`, `pmid` — are ANDed into a Solr query automatically
41
+ - `doi` and `pmid` translate to `doi-self` and `pmid-self` field queries: "who has linked this work to their ORCID record?"
42
+ - `query` appends raw Solr to the generated clause for advanced use
43
+ - `ror_id` values (full URLs like `https://ror.org/00f54p054`) are quoted internally to handle Solr's colon parsing
44
+ - Returns expanded-search results with inline name and institution data — no follow-up profile fetch needed for basic discovery
45
+ - Use this for precise field-anchored lookups; use `orcid_resolve_researcher` for ambiguous names needing ranked disambiguation
46
+
47
+ ---
48
+
49
+ ### `orcid_get_profile`
50
+
51
+ Fetch a researcher's public person section by ORCID iD.
52
+
53
+ - Accepts bare ORCID iD (`0000-0001-2345-6789`) or full URI form
54
+ - Returns name, biography, keywords, researcher URLs, addresses, and external identifiers (Scopus Author ID, ResearcherID, Loop, etc.)
55
+ - External identifiers are embedded in the person response — no separate round-trip
56
+ - Entry point for building a researcher dossier before fetching works or affiliations
57
+
58
+ ---
59
+
60
+ ### `orcid_get_works`
61
+
62
+ Retrieve the works list for a researcher.
63
+
64
+ - Returns work summaries: title, type, publication date, journal name, and all external identifiers (DOI, PMID, arXiv ID, ISBN, etc.)
65
+ - External IDs are returned in formats consumable by downstream servers (Crossref, PubMed, arXiv)
66
+ - Works list is summaries only — chain to the relevant server for full metadata or abstracts
67
+
68
+ ---
69
+
70
+ ### `orcid_get_affiliations`
71
+
72
+ Fetch affiliation records by type, using a single `/activities` call filtered client-side.
73
+
74
+ - `types` controls which sections to include: `employment`, `education`, `invited-positions`, `distinctions`, `memberships`, `qualifications`, `services`, or `all`
75
+ - Default is `['employment', 'education']` (the 90% case)
76
+ - Returns organization names, disambiguated org IDs (ROR/GRID/Ringgold), departments, roles, and date ranges
77
+ - One upstream call regardless of how many types are requested — the `/activities` endpoint returns all sections at once
78
+
79
+ ---
80
+
81
+ ### `orcid_get_funding`
82
+
83
+ Fetch funding records for a researcher.
84
+
85
+ - Returns grants, contracts, awards, and salary awards with funder names, grant numbers, and funding periods
86
+ - Funding data is self-reported and often sparse — absence does not mean no funding
87
+ - Useful when it exists; high-value (grant numbers, funder IDs) but a thin single-endpoint wrapper
88
+
89
+ ---
90
+
91
+ ### `orcid_get_peer_reviews`
92
+
93
+ Fetch peer review activity for a researcher.
94
+
95
+ - Returns convening organizations (journals/publishers), reviewer role (`reviewer`, `editor`, `chair`, etc.), review type, completion dates, and ISSN-keyed group identifiers
96
+ - Useful for assessing editorial activity and journal affiliations
97
+
98
+ ---
99
+
100
+ ### `orcid_resolve_researcher`
101
+
102
+ Disambiguate an author name to a verified ORCID iD.
103
+
104
+ - Returns up to 5 ranked candidates with transparent disambiguation signals: name match type (`exact`/`partial`/`other-name`), institution overlap flag, and anchor type (`doi`/`pmid`/`none`)
105
+ - When `doi` or `pmid` is provided, uses `doi-self` or `pmid-self` as an anchor — researchers who have linked that work to their ORCID record are near-deterministic matches
106
+ - Falls back to a relaxed query (dropping affiliation) if the initial candidate set is empty
107
+ - No synthetic scores — raw signal fields only, so callers can apply their own ranking logic
108
+
109
+ ## Resources
110
+
111
+ | Type | Name | Description |
112
+ |:-----|:-----|:------------|
113
+ | Resource | `orcid://researcher/{orcid_id}/profile` | Researcher profile (person section: name, bio, keywords, external IDs). Prefer the tool when the response needs to flow into conditional logic. |
114
+ | Resource | `orcid://researcher/{orcid_id}/works` | Works list for a researcher. DOIs and PMIDs in the response are ready for Crossref/PubMed chaining. |
115
+
116
+ All resource data is also reachable via tools. Use resources when injecting stable researcher context into a prompt; use tools when filtering or processing results is needed.
117
+
118
+ ## Features
119
+
120
+ Built on [`@cyanheads/mcp-ts-core`](https://www.npmjs.com/package/@cyanheads/mcp-ts-core):
121
+
122
+ - Declarative tool and resource definitions — single file per primitive, framework handles registration and validation
123
+ - Unified error handling — handlers throw, framework catches, classifies, and formats
124
+ - Pluggable auth: `none`, `jwt`, `oauth`
125
+ - Swappable storage backends: `in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`
126
+ - Structured logging with optional OpenTelemetry tracing
127
+ - STDIO and Streamable HTTP transports
128
+
129
+ ORCID-specific:
130
+
131
+ - ORCID Public API v3.0 (`https://pub.orcid.org/v3.0/`) — no API key required for public read endpoints
132
+ - `expanded-search` as the primary search backend — returns ORCID iD, name, and institution data inline, eliminating N+1 profile fetches
133
+ - Single `/activities` call for affiliation queries, filtered client-side — eliminates up to 7 parallel upstream calls vs. per-section fetching
134
+ - External identifiers (DOIs, PMIDs, arXiv IDs) surfaced in works responses in formats ready for cross-server chaining
135
+
136
+ Agent-friendly output:
137
+
138
+ - Transparent disambiguation signals in `orcid_resolve_researcher` — name match type, institution overlap, and anchor type are returned as raw fields, not a synthetic score, so agents can reason about match confidence
139
+ - Known-limitation annotations — works list surfaces `num_found` so agents know when the 10,000-result public API cap was hit; funding and profile tools note when sections are empty due to researcher-controlled visibility
140
+ - External identifier pass-through — DOIs, PMIDs, arXiv IDs, Scopus Author IDs are normalized to formats consumable by downstream servers without parsing
141
+
142
+ ## Getting started
143
+
144
+ Add the following to your MCP client configuration file. No API key is required — the ORCID Public API is open for public read access.
145
+
146
+ ```json
147
+ {
148
+ "mcpServers": {
149
+ "orcid": {
150
+ "type": "stdio",
151
+ "command": "bunx",
152
+ "args": ["@cyanheads/orcid-mcp-server@latest"],
153
+ "env": {
154
+ "MCP_TRANSPORT_TYPE": "stdio",
155
+ "MCP_LOG_LEVEL": "info"
156
+ }
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ Or with npx (no Bun required):
163
+
164
+ ```json
165
+ {
166
+ "mcpServers": {
167
+ "orcid": {
168
+ "type": "stdio",
169
+ "command": "npx",
170
+ "args": ["-y", "@cyanheads/orcid-mcp-server@latest"],
171
+ "env": {
172
+ "MCP_TRANSPORT_TYPE": "stdio",
173
+ "MCP_LOG_LEVEL": "info"
174
+ }
175
+ }
176
+ }
177
+ }
178
+ ```
179
+
180
+ Or with Docker:
181
+
182
+ ```json
183
+ {
184
+ "mcpServers": {
185
+ "orcid": {
186
+ "type": "stdio",
187
+ "command": "docker",
188
+ "args": [
189
+ "run", "-i", "--rm",
190
+ "-e", "MCP_TRANSPORT_TYPE=stdio",
191
+ "ghcr.io/cyanheads/orcid-mcp-server:latest"
192
+ ]
193
+ }
194
+ }
195
+ }
196
+ ```
197
+
198
+ For Streamable HTTP, set the transport and start the server:
199
+
200
+ ```sh
201
+ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
202
+ # Server listens at http://localhost:3010/mcp
203
+ ```
204
+
205
+ ### Prerequisites
206
+
207
+ - [Bun v1.3.2](https://bun.sh/) or higher (or Node.js v24+).
208
+ - No API key required. The ORCID Public API is open for public read access. Non-commercial use only under [ORCID Public API ToS §2](https://info.orcid.org/public-client-terms-of-service/).
209
+
210
+ ### Installation
211
+
212
+ 1. **Clone the repository:**
213
+
214
+ ```sh
215
+ git clone https://github.com/cyanheads/orcid-mcp-server.git
216
+ ```
217
+
218
+ 2. **Navigate into the directory:**
219
+
220
+ ```sh
221
+ cd orcid-mcp-server
222
+ ```
223
+
224
+ 3. **Install dependencies:**
225
+
226
+ ```sh
227
+ bun install
228
+ ```
229
+
230
+ 4. **Configure environment:**
231
+
232
+ ```sh
233
+ cp .env.example .env
234
+ # edit .env if needed — no required vars
235
+ ```
236
+
237
+ ## Configuration
238
+
239
+ All configuration is validated at startup via Zod schemas in `src/config/server-config.ts`. Key environment variables:
240
+
241
+ | Variable | Description | Default |
242
+ |:---------|:------------|:--------|
243
+ | `ORCID_API_BASE_URL` | Override the ORCID API base URL. Useful for pointing at the sandbox (`https://pub.sandbox.orcid.org/v3.0/`). | `https://pub.orcid.org/v3.0/` |
244
+ | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http` | `stdio` |
245
+ | `MCP_HTTP_PORT` | HTTP server port | `3010` |
246
+ | `MCP_HTTP_ENDPOINT_PATH` | HTTP endpoint path | `/mcp` |
247
+ | `MCP_PUBLIC_URL` | Public origin for TLS-terminating reverse-proxy deployments | none |
248
+ | `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth` | `none` |
249
+ | `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.) | `info` |
250
+ | `MCP_GC_PRESSURE_INTERVAL_MS` | Opt-in Bun-only forced-GC pressure loop (ms). Try `60000` if heap growth is observed under sustained HTTP load. | `0` (disabled) |
251
+ | `LOGS_DIR` | Directory for log files (Node.js only) | `<project-root>/logs` |
252
+ | `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1` | `in-memory` |
253
+ | `OTEL_ENABLED` | Enable OpenTelemetry | `false` |
254
+
255
+ See [`.env.example`](./.env.example) for the full list of optional overrides.
256
+
257
+ ## Running the server
258
+
259
+ ### Local development
260
+
261
+ - **Build and run:**
262
+
263
+ ```sh
264
+ # One-time build
265
+ bun run rebuild
266
+
267
+ # Run the built server
268
+ bun run start:stdio
269
+ # or
270
+ bun run start:http
271
+ ```
272
+
273
+ - **Run checks and tests:**
274
+
275
+ ```sh
276
+ bun run devcheck # Lint, format, typecheck, security
277
+ bun run test # Vitest test suite
278
+ bun run lint:mcp # Validate MCP definitions against spec
279
+ ```
280
+
281
+ ### Docker
282
+
283
+ ```sh
284
+ docker build -t orcid-mcp-server .
285
+ docker run --rm -p 3010:3010 orcid-mcp-server
286
+ ```
287
+
288
+ The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `/var/log/orcid-mcp-server`. OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
289
+
290
+ ## Project structure
291
+
292
+ | Directory | Purpose |
293
+ |:----------|:--------|
294
+ | `src/index.ts` | `createApp()` entry point — registers tools and resources, inits services. |
295
+ | `src/config` | Server-specific environment variable parsing and validation with Zod. |
296
+ | `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Seven tools across search, profile, works, affiliations, funding, peer reviews, and disambiguation. |
297
+ | `src/mcp-server/resources` | Resource definitions (`*.resource.ts`). Profile and works resources. |
298
+ | `src/services/orcid` | ORCID Public API v3.0 service layer — search, record section fetchers, retry/backoff. |
299
+ | `tests/` | Unit and integration tests mirroring `src/`. |
300
+
301
+ ## Development guide
302
+
303
+ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
304
+
305
+ - Handlers throw, framework catches — no `try/catch` in tool logic
306
+ - Use `ctx.log` for request-scoped logging, `ctx.state` for tenant-scoped storage
307
+ - Register new tools and resources in the `createApp()` arrays
308
+ - Wrap ORCID API calls: validate raw response → normalize to domain type → return output schema; never fabricate missing fields
309
+
310
+ ## Contributing
311
+
312
+ Issues and pull requests are welcome. Run checks and tests before submitting:
313
+
314
+ ```sh
315
+ bun run devcheck
316
+ bun run test
317
+ ```
318
+
319
+ ## License
320
+
321
+ Apache-2.0 — see [LICENSE](./LICENSE) for details.
@@ -0,0 +1,20 @@
1
+ ---
2
+ summary: "Initial release — 7 tools and 2 resources for the ORCID Public API v3.0"
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.0 — 2026-05-25
8
+
9
+ ## Added
10
+
11
+ - **`orcid_search_researchers`** — search the ORCID registry with structured field params (`given_name`, `family_name`, `affiliation`, `keyword`, `ror_id`, `doi`, `pmid`) mapped to Solr queries via `expanded-search`
12
+ - **`orcid_get_profile`** — fetch the person section of a public ORCID record: name, biography, keywords, researcher URLs, and external identifiers (Scopus, ResearcherID, Loop, etc.)
13
+ - **`orcid_get_works`** — retrieve work summaries with all external identifiers (DOI, PMID, arXiv ID) in formats ready for cross-server chaining
14
+ - **`orcid_get_affiliations`** — fetch affiliation records filtered by type (`employment`, `education`, `invited-positions`, `distinctions`, `memberships`, `qualifications`, `services`, `all`) via a single `/activities` call
15
+ - **`orcid_get_funding`** — fetch funding records (grants, contracts, awards) with funder names and grant numbers
16
+ - **`orcid_get_peer_reviews`** — fetch peer review activity with convening organizations, reviewer role, review type, and ISSN-keyed group identifiers
17
+ - **`orcid_resolve_researcher`** — disambiguate an author name to a verified ORCID iD; returns ranked candidates with transparent signals (name match type, institution overlap, anchor type)
18
+ - **`orcid://researcher/{orcid_id}/profile`** resource — researcher person section as stable inline context
19
+ - **`orcid://researcher/{orcid_id}/works`** resource — researcher works list with external IDs as stable inline context
20
+ - Scaffolded on `@cyanheads/mcp-ts-core ^0.9.9`
@@ -0,0 +1,13 @@
1
+ ---
2
+ summary: "ORCID iD format validation, 404 error contract fix, description cleanup"
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.1 — 2026-05-25
8
+
9
+ ## Fixed
10
+
11
+ - **ORCID iD format validation** — all tools and resources now reject malformed iDs at input parse time via a regex (`^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$` or full URI form) instead of letting bad input reach the API
12
+ - **Error contract not firing** — `orcid_get_profile`, `orcid_get_works`, `orcid_get_affiliations`, `orcid_get_funding`, `orcid_get_peer_reviews` now route ORCID 404s through `ctx.fail('profile_not_found', …)` so `data.reason` is present and correlated with the declared error contract
13
+ - **Tool description noise** — `orcid_search_researchers` and `orcid_get_affiliations` descriptions trimmed of internal Solr field names and implementation notes not relevant to callers
@@ -0,0 +1,119 @@
1
+ ---
2
+ # FORMAT REFERENCE — do not edit. Copy this file to
3
+ # `changelog/<major.minor>.x/<version>.md` (e.g. `changelog/0.8.x/0.8.6.md`)
4
+ # to author a new release. Set that file's H1 to `# <version> — YYYY-MM-DD`
5
+ # with a concrete date.
6
+
7
+ # Required. One-line GitHub Release-style headline. 350 character cap.
8
+ # Default short and scannable. Don't pad, don't stitch unrelated changes with
9
+ # semicolons — pick the headline. Quotes required: unquoted YAML treats `: `
10
+ # inside the value as a key separator and fails GitHub's strict parser.
11
+ summary: ""
12
+
13
+ # Set `true` when consumers must change code to upgrade: API removals,
14
+ # signature changes, config renames, behavior changes that break existing
15
+ # usage. Flagged as `Breaking` in the rollup.
16
+ breaking: false
17
+
18
+ # Set `true` if this release contains any security fix. Pairs with the
19
+ # `## Security` section below. Flagged as `Security` in the rollup so
20
+ # users can triage upgrade urgency at a glance.
21
+ security: false
22
+ ---
23
+
24
+ # <version> — YYYY-MM-DD
25
+
26
+ <!--
27
+ AUTHORING GUIDE — applies to the new per-version file you create from this
28
+ template.
29
+
30
+ Audience: someone scanning release notes to decide what affects them. Lead
31
+ each bullet with the symbol or concept name in **bold** so they can skip
32
+ what's irrelevant and zoom in on what's not.
33
+
34
+ Tone: terse, fact-dense, not verbose. Default to one sentence per bullet —
35
+ name the symbol, state what changed, stop. Use a second sentence only when
36
+ it carries weight. If a bullet feels long, it is.
37
+
38
+ Cut: mechanism walkthroughs (those belong in JSDoc, AGENTS.md, or the
39
+ relevant skill), ceremonial framings ("This release introduces…",
40
+ backwards-compat paragraphs), file-by-file test enumerations, internal
41
+ implementation notes. Prefer code/symbol names over English re-explanations.
42
+
43
+ Narrative intro: skip by default. Add one short sentence only when the
44
+ release theme genuinely needs framing the bullets can't carry.
45
+
46
+ Sections: Keep a Changelog order — Added, Changed, Deprecated, Removed,
47
+ Fixed, Security. Include only sections with entries; delete the rest
48
+ (including the commented-out scaffolding below). Don't ship empty headers.
49
+
50
+ Include: every distinct fact a reader needs to adopt or audit the release —
51
+ new exports, signatures, lint rule IDs, env vars, breaking changes, version
52
+ bumps on shipped skills. Nothing more.
53
+
54
+ Links: link issues, PRs, docs, or skills where they help a reader jump to
55
+ context. Once per item per entry — don't re-link the same issue in summary,
56
+ narrative, and bullet. Skip links for inline symbol names; code spans speak
57
+ for themselves.
58
+
59
+ Issue/PR URLs: use full URLs. GitHub's bare `#NN` auto-link only resolves
60
+ inside its own UI, not in npm reads or local editors.
61
+
62
+ [#38](https://github.com/cyanheads/mcp-ts-core/issues/38) ← issue
63
+ [#42](https://github.com/cyanheads/mcp-ts-core/pull/42) ← PR
64
+
65
+ Verify numbers exist before linking (`gh issue view NN`, `gh pr view NN`).
66
+ Never speculate on a future number — `#42` for an upcoming PR silently
67
+ resolves to whatever real item already owns 42, and timeline previews pull
68
+ in that unrelated item's metadata.
69
+
70
+ TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
71
+ via `gh release create --notes-from-tag`. The tag is a derivative of this
72
+ changelog entry — a condensed, scannable version, not a copy. Format:
73
+
74
+ <theme — omit version number, GitHub prepends it>
75
+ ← blank line
76
+ <1-2 sentence context: what this release does>
77
+ ← blank line
78
+ Dependency bumps: ← section header
79
+ ← blank line
80
+ - `@cyanheads/mcp-ts-core` ^0.9.1 → ^0.9.6 ← bullet
81
+ ← blank line
82
+ Changed: ← only sections with entries
83
+ ← blank line
84
+ - `format()` output includes `query` in text mode
85
+ ← blank line
86
+ Added:
87
+ ← blank line
88
+ - `manifest.json` scaffolded for MCPB bundle support
89
+ - Install badges (Claude Desktop, Cursor, VS Code)
90
+ ← blank line
91
+ <N> tests pass; `bun run devcheck` clean. ← footer
92
+
93
+ Never a flat comma-separated string. Always structured markdown with
94
+ sections. The tag must scan well as a rendered GitHub Release page.
95
+ -->
96
+
97
+ ## Added
98
+
99
+ -
100
+
101
+ ## Changed
102
+
103
+ -
104
+
105
+ <!-- ## Deprecated
106
+
107
+ - -->
108
+
109
+ <!-- ## Removed
110
+
111
+ - -->
112
+
113
+ ## Fixed
114
+
115
+ -
116
+
117
+ <!-- ## Security
118
+
119
+ - -->
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview Server-specific environment variable configuration for orcid-mcp-server.
3
+ * @module config/server-config
4
+ */
5
+ import { z } from '@cyanheads/mcp-ts-core';
6
+ declare const ServerConfigSchema: z.ZodObject<{
7
+ orcidApiBaseUrl: z.ZodDefault<z.ZodString>;
8
+ }, z.core.$strip>;
9
+ /** Returns the parsed server configuration, lazily initialized on first call. */
10
+ export declare function getServerConfig(): z.infer<typeof ServerConfigSchema>;
11
+ export {};
12
+ //# sourceMappingURL=server-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;iBAMtB,CAAC;AAIH,iFAAiF;AACjF,wBAAgB,eAAe,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAKpE"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @fileoverview Server-specific environment variable configuration for orcid-mcp-server.
3
+ * @module config/server-config
4
+ */
5
+ import { z } from '@cyanheads/mcp-ts-core';
6
+ import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
7
+ const ServerConfigSchema = z.object({
8
+ orcidApiBaseUrl: z
9
+ .string()
10
+ .url()
11
+ .default('https://pub.orcid.org/v3.0')
12
+ .describe('ORCID Public API base URL (default: https://pub.orcid.org/v3.0)'),
13
+ });
14
+ let _config;
15
+ /** Returns the parsed server configuration, lazily initialized on first call. */
16
+ export function getServerConfig() {
17
+ _config ??= parseEnvConfig(ServerConfigSchema, {
18
+ orcidApiBaseUrl: 'ORCID_API_BASE_URL',
19
+ });
20
+ return _config;
21
+ }
22
+ //# sourceMappingURL=server-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,eAAe,EAAE,CAAC;SACf,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,4BAA4B,CAAC;SACrC,QAAQ,CAAC,iEAAiE,CAAC;CAC/E,CAAC,CAAC;AAEH,IAAI,OAAuD,CAAC;AAE5D,iFAAiF;AACjF,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,eAAe,EAAE,oBAAoB;KACtC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview orcid-mcp-server MCP server entry point. Provides access to the
4
+ * ORCID researcher registry: search, profiles, works, affiliations, funding, and peer reviews.
5
+ * @module index
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;GAIG"}
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview orcid-mcp-server MCP server entry point. Provides access to the
4
+ * ORCID researcher registry: search, profiles, works, affiliations, funding, and peer reviews.
5
+ * @module index
6
+ */
7
+ import { createApp } from '@cyanheads/mcp-ts-core';
8
+ import { researcherProfileResource } from './mcp-server/resources/definitions/researcher-profile.resource.js';
9
+ import { researcherWorksResource } from './mcp-server/resources/definitions/researcher-works.resource.js';
10
+ import { orcidGetAffiliations } from './mcp-server/tools/definitions/get-affiliations.tool.js';
11
+ import { orcidGetFunding } from './mcp-server/tools/definitions/get-funding.tool.js';
12
+ import { orcidGetPeerReviews } from './mcp-server/tools/definitions/get-peer-reviews.tool.js';
13
+ import { orcidGetProfile } from './mcp-server/tools/definitions/get-profile.tool.js';
14
+ import { orcidGetWorks } from './mcp-server/tools/definitions/get-works.tool.js';
15
+ import { orcidResolveResearcher } from './mcp-server/tools/definitions/resolve-researcher.tool.js';
16
+ import { orcidSearchResearchers } from './mcp-server/tools/definitions/search-researchers.tool.js';
17
+ import { initOrcidService } from './services/orcid/orcid-service.js';
18
+ await createApp({
19
+ tools: [
20
+ orcidGetProfile,
21
+ orcidSearchResearchers,
22
+ orcidGetWorks,
23
+ orcidGetAffiliations,
24
+ orcidGetFunding,
25
+ orcidGetPeerReviews,
26
+ orcidResolveResearcher,
27
+ ],
28
+ resources: [researcherProfileResource, researcherWorksResource],
29
+ prompts: [],
30
+ setup(core) {
31
+ initOrcidService(core.config, core.storage);
32
+ },
33
+ instructions: 'ORCID researcher registry server. Use orcid_search_researchers for exact field lookups (name + institution + DOI/PMID). Use orcid_resolve_researcher when the input is an ambiguous author name needing ranked disambiguation. Use orcid_get_profile → orcid_get_works → orcid_get_affiliations to build a researcher dossier. DOIs and PMIDs from orcid_get_works are ready for chaining to Crossref or PubMed servers.',
34
+ });
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,yBAAyB,EAAE,MAAM,mEAAmE,CAAC;AAC9G,OAAO,EAAE,uBAAuB,EAAE,MAAM,iEAAiE,CAAC;AAC1G,OAAO,EAAE,oBAAoB,EAAE,MAAM,yDAAyD,CAAC;AAC/F,OAAO,EAAE,eAAe,EAAE,MAAM,oDAAoD,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,yDAAyD,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,oDAAoD,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,kDAAkD,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,2DAA2D,CAAC;AACnG,OAAO,EAAE,sBAAsB,EAAE,MAAM,2DAA2D,CAAC;AACnG,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAErE,MAAM,SAAS,CAAC;IACd,KAAK,EAAE;QACL,eAAe;QACf,sBAAsB;QACtB,aAAa;QACb,oBAAoB;QACpB,eAAe;QACf,mBAAmB;QACnB,sBAAsB;KACvB;IACD,SAAS,EAAE,CAAC,yBAAyB,EAAE,uBAAuB,CAAC;IAC/D,OAAO,EAAE,EAAE;IACX,KAAK,CAAC,IAAI;QACR,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IACD,YAAY,EACV,0ZAA0Z;CAC7Z,CAAC,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @fileoverview Resource for injecting a researcher's ORCID profile (person section)
3
+ * as stable inline context into prompts. Use when the agent needs a researcher's
4
+ * identity data without conditional logic over the result.
5
+ * @module mcp-server/resources/definitions/researcher-profile.resource
6
+ */
7
+ import { z } from '@cyanheads/mcp-ts-core';
8
+ export declare const researcherProfileResource: import("@cyanheads/mcp-ts-core").ResourceDefinition<z.ZodObject<{
9
+ orcid_id: z.ZodString;
10
+ }, z.core.$strip>, z.ZodObject<{
11
+ orcidId: z.ZodString;
12
+ orcidUri: z.ZodString;
13
+ givenNames: z.ZodOptional<z.ZodString>;
14
+ familyName: z.ZodOptional<z.ZodString>;
15
+ creditName: z.ZodOptional<z.ZodString>;
16
+ biography: z.ZodOptional<z.ZodString>;
17
+ keywords: z.ZodArray<z.ZodString>;
18
+ externalIdentifiers: z.ZodArray<z.ZodObject<{
19
+ type: z.ZodString;
20
+ value: z.ZodString;
21
+ url: z.ZodOptional<z.ZodString>;
22
+ }, z.core.$strip>>;
23
+ }, z.core.$strip>, undefined>;
24
+ //# sourceMappingURL=researcher-profile.resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"researcher-profile.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/researcher-profile.resource.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAIrD,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;6BAoEpC,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @fileoverview Resource for injecting a researcher's ORCID profile (person section)
3
+ * as stable inline context into prompts. Use when the agent needs a researcher's
4
+ * identity data without conditional logic over the result.
5
+ * @module mcp-server/resources/definitions/researcher-profile.resource
6
+ */
7
+ import { resource, z } from '@cyanheads/mcp-ts-core';
8
+ import { notFound } from '@cyanheads/mcp-ts-core/errors';
9
+ import { getOrcidService, normalizeOrcidId } from '../../../services/orcid/orcid-service.js';
10
+ export const researcherProfileResource = resource('orcid://researcher/{orcid_id}/profile', {
11
+ name: 'orcid-researcher-profile',
12
+ description: 'Researcher profile (person section) from ORCID: name, biography, keywords, researcher URLs, and external identifiers. Use when injecting researcher identity context into a prompt or checking for a specific external ID (e.g., Scopus ID for cross-server chaining). Prefer the orcid_get_profile tool when the response needs to flow into conditional logic.',
13
+ mimeType: 'application/json',
14
+ params: z.object({
15
+ orcid_id: z
16
+ .string()
17
+ .regex(/^(https?:\/\/orcid\.org\/)?\d{4}-\d{4}-\d{4}-\d{3}[\dX]$/, 'Must be a valid ORCID iD (e.g. 0000-0001-2345-6789) or full ORCID URI.')
18
+ .describe('ORCID iD — bare format (0000-0001-2345-6789) or full URI (https://orcid.org/0000-0001-2345-6789).'),
19
+ }),
20
+ output: z.object({
21
+ orcidId: z.string().describe('Normalized ORCID iD (bare format).'),
22
+ orcidUri: z.string().describe('Full ORCID URI.'),
23
+ givenNames: z.string().optional().describe('Given names, if publicly visible.'),
24
+ familyName: z.string().optional().describe('Family name, if publicly visible.'),
25
+ creditName: z.string().optional().describe('Published credit name, if set.'),
26
+ biography: z.string().optional().describe('Researcher biography, if publicly visible.'),
27
+ keywords: z.array(z.string().describe('Keyword.')).describe('Research keywords.'),
28
+ externalIdentifiers: z
29
+ .array(z
30
+ .object({
31
+ type: z.string().describe('Identifier type.'),
32
+ value: z.string().describe('Identifier value.'),
33
+ url: z.string().optional().describe('Resolver URL.'),
34
+ })
35
+ .describe('External identifier.'))
36
+ .describe('External scholarly identifiers (Scopus, ResearcherID, Loop, etc.).'),
37
+ }),
38
+ async handler(params, ctx) {
39
+ const service = getOrcidService();
40
+ const bareId = normalizeOrcidId(params.orcid_id);
41
+ ctx.log.debug('orcid-researcher-profile resource', { orcidId: bareId });
42
+ const person = await service.getPerson(params.orcid_id, ctx);
43
+ if (!person.givenNames && !person.familyName && !person.creditName) {
44
+ throw notFound(`No public profile found for ORCID iD ${bareId}. The record may not exist or may be fully private.`);
45
+ }
46
+ return {
47
+ orcidId: bareId,
48
+ orcidUri: `https://orcid.org/${bareId}`,
49
+ ...(person.givenNames && { givenNames: person.givenNames }),
50
+ ...(person.familyName && { familyName: person.familyName }),
51
+ ...(person.creditName && { creditName: person.creditName }),
52
+ ...(person.biography && { biography: person.biography }),
53
+ keywords: person.keywords,
54
+ externalIdentifiers: person.externalIdentifiers.map((id) => ({
55
+ type: id.type,
56
+ value: id.value,
57
+ ...(id.url && { url: id.url }),
58
+ })),
59
+ };
60
+ },
61
+ });
62
+ //# sourceMappingURL=researcher-profile.resource.js.map