clinicaltrialsgov-mcp-server 2.9.7 → 2.9.9
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.
- package/AGENTS.md +484 -0
- package/CLAUDE.md +484 -0
- package/Dockerfile +116 -0
- package/README.md +196 -102
- package/changelog/2.0.x/2.0.0.md +65 -0
- package/changelog/2.0.x/2.0.1.md +18 -0
- package/changelog/2.0.x/2.0.2.md +18 -0
- package/changelog/2.0.x/2.0.3.md +15 -0
- package/changelog/2.0.x/2.0.4.md +10 -0
- package/changelog/2.0.x/2.0.5.md +10 -0
- package/changelog/2.0.x/2.0.6.md +24 -0
- package/changelog/2.1.x/2.1.0.md +25 -0
- package/changelog/2.1.x/2.1.1.md +22 -0
- package/changelog/2.2.x/2.2.0.md +23 -0
- package/changelog/2.3.x/2.3.0.md +35 -0
- package/changelog/2.3.x/2.3.1.md +14 -0
- package/changelog/2.3.x/2.3.2.md +10 -0
- package/changelog/2.3.x/2.3.3.md +18 -0
- package/changelog/2.3.x/2.3.4.md +36 -0
- package/changelog/2.3.x/2.3.5.md +21 -0
- package/changelog/2.4.x/2.4.0.md +36 -0
- package/changelog/2.4.x/2.4.1.md +19 -0
- package/changelog/2.4.x/2.4.10.md +21 -0
- package/changelog/2.4.x/2.4.11.md +12 -0
- package/changelog/2.4.x/2.4.12.md +18 -0
- package/changelog/2.4.x/2.4.2.md +16 -0
- package/changelog/2.4.x/2.4.3.md +34 -0
- package/changelog/2.4.x/2.4.4.md +20 -0
- package/changelog/2.4.x/2.4.5.md +31 -0
- package/changelog/2.4.x/2.4.6.md +32 -0
- package/changelog/2.4.x/2.4.7.md +39 -0
- package/changelog/2.4.x/2.4.8.md +23 -0
- package/changelog/2.4.x/2.4.9.md +23 -0
- package/changelog/2.5.x/2.5.0.md +20 -0
- package/changelog/2.5.x/2.5.1.md +17 -0
- package/changelog/2.5.x/2.5.2.md +24 -0
- package/changelog/2.5.x/2.5.3.md +19 -0
- package/changelog/2.5.x/2.5.4.md +16 -0
- package/changelog/2.5.x/2.5.5.md +22 -0
- package/changelog/2.6.x/2.6.0.md +29 -0
- package/changelog/2.6.x/2.6.1.md +15 -0
- package/changelog/2.6.x/2.6.2.md +22 -0
- package/changelog/2.6.x/2.6.3.md +12 -0
- package/changelog/2.6.x/2.6.4.md +19 -0
- package/changelog/2.6.x/2.6.5.md +18 -0
- package/changelog/2.6.x/2.6.6.md +28 -0
- package/changelog/2.7.x/2.7.0.md +21 -0
- package/changelog/2.7.x/2.7.1.md +16 -0
- package/changelog/2.7.x/2.7.2.md +19 -0
- package/changelog/2.7.x/2.7.3.md +28 -0
- package/changelog/2.7.x/2.7.4.md +16 -0
- package/changelog/2.8.x/2.8.0.md +22 -0
- package/changelog/2.8.x/2.8.1.md +37 -0
- package/changelog/2.8.x/2.8.2.md +11 -0
- package/changelog/2.8.x/2.8.3.md +30 -0
- package/changelog/2.8.x/2.8.4.md +16 -0
- package/changelog/2.8.x/2.8.5.md +12 -0
- package/changelog/2.8.x/2.8.6.md +33 -0
- package/changelog/2.8.x/2.8.7.md +20 -0
- package/changelog/2.9.x/2.9.0.md +22 -0
- package/changelog/2.9.x/2.9.1.md +20 -0
- package/changelog/2.9.x/2.9.2.md +18 -0
- package/changelog/2.9.x/2.9.3.md +31 -0
- package/changelog/2.9.x/2.9.4.md +34 -0
- package/changelog/2.9.x/2.9.5.md +22 -0
- package/changelog/2.9.x/2.9.6.md +22 -0
- package/changelog/2.9.x/2.9.7.md +28 -0
- package/changelog/2.9.x/2.9.8.md +26 -0
- package/changelog/2.9.x/2.9.9.md +25 -0
- package/changelog/template.md +151 -0
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/resources/definitions/study.resource.d.ts +3 -1
- package/dist/mcp-server/resources/definitions/study.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/study.resource.js +2 -0
- package/dist/mcp-server/resources/definitions/study.resource.js.map +1 -1
- package/dist/mcp-server/tools/definitions/find-eligible.tool.d.ts +1 -0
- package/dist/mcp-server/tools/definitions/find-eligible.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/find-eligible.tool.js +1 -0
- package/dist/mcp-server/tools/definitions/find-eligible.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-field-definitions.tool.d.ts +1 -0
- package/dist/mcp-server/tools/definitions/get-field-definitions.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-field-definitions.tool.js +1 -0
- package/dist/mcp-server/tools/definitions/get-field-definitions.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-field-values.tool.d.ts +2 -0
- package/dist/mcp-server/tools/definitions/get-field-values.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-field-values.tool.js +2 -0
- package/dist/mcp-server/tools/definitions/get-field-values.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-study-count.tool.d.ts +4 -0
- package/dist/mcp-server/tools/definitions/get-study-count.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-study-count.tool.js +12 -4
- package/dist/mcp-server/tools/definitions/get-study-count.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-study-results.tool.d.ts +1 -1
- package/dist/mcp-server/tools/definitions/get-study-results.tool.js +1 -1
- package/dist/mcp-server/tools/definitions/get-study-results.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-study.tool.d.ts +3 -1
- package/dist/mcp-server/tools/definitions/get-study.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-study.tool.js +2 -0
- package/dist/mcp-server/tools/definitions/get-study.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-studies.tool.d.ts +7 -1
- package/dist/mcp-server/tools/definitions/search-studies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-studies.tool.js +42 -12
- package/dist/mcp-server/tools/definitions/search-studies.tool.js.map +1 -1
- package/dist/mcp-server/tools/utils/_schemas.d.ts +9 -2
- package/dist/mcp-server/tools/utils/_schemas.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/_schemas.js +9 -2
- package/dist/mcp-server/tools/utils/_schemas.js.map +1 -1
- package/dist/mcp-server/tools/utils/query-helpers.d.ts +21 -1
- package/dist/mcp-server/tools/utils/query-helpers.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/query-helpers.js +29 -1
- package/dist/mcp-server/tools/utils/query-helpers.js.map +1 -1
- package/package.json +19 -10
- package/server.json +147 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# Agent Protocol
|
|
2
|
+
|
|
3
|
+
**Server:** clinicaltrialsgov-mcp-server
|
|
4
|
+
**Version:** 2.9.9
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
|
|
6
|
+
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
|
|
8
|
+
**Zod:** ^4.6.5
|
|
9
|
+
|
|
10
|
+
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
MCP server wrapping the [ClinicalTrials.gov REST API v2](https://clinicaltrials.gov/data-api/api) — the US National Library of Medicine's registry of 600K+ clinical trial studies. Public, read-only, no auth required.
|
|
17
|
+
|
|
18
|
+
**Design doc:** `docs/design.md` — full MCP surface design, tool schemas, service plan, implementation checklist.
|
|
19
|
+
**API reference:** `docs/api-reference.md` — complete ClinicalTrials.gov v2 endpoint reference.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## MCP Surface
|
|
24
|
+
|
|
25
|
+
### Tools (7)
|
|
26
|
+
|
|
27
|
+
| Name | Description |
|
|
28
|
+
| :------------------------------------- | :---------------------------------------------------------------------------------- |
|
|
29
|
+
| `clinicaltrials_search_studies` | Search studies with queries, filters, pagination, field selection. Primary tool. |
|
|
30
|
+
| `clinicaltrials_get_study_record` | Single study by NCT ID. Tool equivalent of the resource for resource-unaware clients. |
|
|
31
|
+
| `clinicaltrials_get_study_results` | Extract outcomes, adverse events, participant flow, baseline for completed studies. |
|
|
32
|
+
| `clinicaltrials_get_field_values` | Discover valid enum values for API fields with study counts. |
|
|
33
|
+
| `clinicaltrials_get_field_definitions` | Browse the study data model field tree — piece names, types, nesting. |
|
|
34
|
+
| `clinicaltrials_get_study_count` | Lightweight study count for a query (no data fetched). |
|
|
35
|
+
| `clinicaltrials_find_eligible` | Match patient demographics to recruiting trials. |
|
|
36
|
+
|
|
37
|
+
### Resources (1)
|
|
38
|
+
|
|
39
|
+
| URI Template | Description |
|
|
40
|
+
| :------------------------- | :--------------------------------------- |
|
|
41
|
+
| `clinicaltrials://{nctId}` | Single study by NCT ID. Bounded protocol record; results replaced by counts. |
|
|
42
|
+
|
|
43
|
+
### Prompts (1)
|
|
44
|
+
|
|
45
|
+
| Name | Description |
|
|
46
|
+
| :------------------------ | :----------------------------------------------------------- |
|
|
47
|
+
| `analyze_trial_landscape` | Guides multi-step trend analysis using count + search tools. |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## What's Next?
|
|
52
|
+
|
|
53
|
+
When the user asks what's next or needs direction, suggest options based on the current project state. Common next steps:
|
|
54
|
+
|
|
55
|
+
1. **Re-run the `setup` skill** — ensures CLAUDE.md, skills, structure, and metadata are populated and up to date with the current codebase
|
|
56
|
+
2. **Run the `design-mcp-server` skill** — if the tool/resource surface hasn't been mapped yet, work through domain design
|
|
57
|
+
3. **Add tools/resources/prompts** — scaffold new definitions using the `add-tool`, `add-app-tool`, `add-resource`, `add-prompt` skills
|
|
58
|
+
4. **Add services** — scaffold domain service integrations using the `add-service` skill
|
|
59
|
+
5. **Add tests** — scaffold tests for existing definitions using the `add-test` skill
|
|
60
|
+
6. **Field-test definitions** — exercise tools/resources/prompts with real inputs using the `field-test` skill, get a report of issues and pain points
|
|
61
|
+
7. **Run `devcheck`** — lint, format, typecheck, and security audit
|
|
62
|
+
8. **Run the `security-pass` skill** — audit handlers for MCP-specific security gaps: output injection, scope blast radius, input sinks, tenant isolation
|
|
63
|
+
9. **Run the `polish-docs-meta` skill** — finalize README, CHANGELOG, metadata, and agent protocol for shipping
|
|
64
|
+
10. **Run the `maintenance` skill** — investigate changelogs, adopt upstream changes, and sync skills after `bun update --latest`
|
|
65
|
+
|
|
66
|
+
Tailor suggestions to what's actually missing or stale — don't recite the full list every time.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Core Rules
|
|
71
|
+
|
|
72
|
+
- **Logic throws, framework catches.** Tool/resource handlers are pure — throw on failure, no `try/catch`. Plain `Error` is fine; the framework catches, classifies, and formats. Use error factories (`notFound()`, `validationError()`, etc.) when the error code matters.
|
|
73
|
+
- **Use `ctx.log`** for request-scoped logging. No `console` calls.
|
|
74
|
+
- **Read-only server.** No `ctx.state` needed — the ClinicalTrials.gov API is stateless and public.
|
|
75
|
+
- **Secrets in env vars only** — never hardcoded. (This server has no secrets — public API, no auth.)
|
|
76
|
+
- **Rate limit awareness.** The API allows ~1 req/sec. Service layer handles retry/backoff.
|
|
77
|
+
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Patterns
|
|
82
|
+
|
|
83
|
+
### Tool
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { tool, z } from "@cyanheads/mcp-ts-core";
|
|
87
|
+
import { getClinicalTrialsService } from "@/services/clinical-trials/clinical-trials-service.js";
|
|
88
|
+
|
|
89
|
+
export const searchStudies = tool("clinicaltrials_search_studies", {
|
|
90
|
+
description: "Search for clinical trial studies from ClinicalTrials.gov.",
|
|
91
|
+
annotations: {
|
|
92
|
+
readOnlyHint: true,
|
|
93
|
+
idempotentHint: true,
|
|
94
|
+
openWorldHint: true,
|
|
95
|
+
},
|
|
96
|
+
input: z.object({
|
|
97
|
+
conditionQuery: z.string().optional().describe("Condition/disease search"),
|
|
98
|
+
pageSize: z
|
|
99
|
+
.number()
|
|
100
|
+
.int()
|
|
101
|
+
.min(1)
|
|
102
|
+
.max(1000)
|
|
103
|
+
.default(10)
|
|
104
|
+
.describe("Results per page"),
|
|
105
|
+
}),
|
|
106
|
+
output: z.object({
|
|
107
|
+
studies: z.array(z.record(z.unknown())).describe("Matching studies"),
|
|
108
|
+
totalCount: z.number().optional().describe("Total matching studies"),
|
|
109
|
+
}),
|
|
110
|
+
|
|
111
|
+
async handler(input, ctx) {
|
|
112
|
+
const service = getClinicalTrialsService();
|
|
113
|
+
const result = await service.searchStudies(
|
|
114
|
+
{ conditionQuery: input.conditionQuery, pageSize: input.pageSize },
|
|
115
|
+
ctx,
|
|
116
|
+
);
|
|
117
|
+
ctx.log.info("Search completed", { count: result.studies?.length });
|
|
118
|
+
return result;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
format: (result) => [
|
|
122
|
+
{ type: "text", text: `Found ${result.studies.length} studies` },
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Resource
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { resource, z } from "@cyanheads/mcp-ts-core";
|
|
131
|
+
import { getClinicalTrialsService } from "@/services/clinical-trials/clinical-trials-service.js";
|
|
132
|
+
|
|
133
|
+
export const studyResource = resource("clinicaltrials://{nctId}", {
|
|
134
|
+
description: "Fetch a single clinical study by NCT ID.",
|
|
135
|
+
mimeType: "application/json",
|
|
136
|
+
params: z.object({
|
|
137
|
+
nctId: z
|
|
138
|
+
.string()
|
|
139
|
+
.regex(/^NCT\d{8}$/)
|
|
140
|
+
.describe("NCT identifier"),
|
|
141
|
+
}),
|
|
142
|
+
|
|
143
|
+
async handler(params, ctx) {
|
|
144
|
+
const service = getClinicalTrialsService();
|
|
145
|
+
return await service.getStudy(params.nctId, ctx);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Prompt
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { prompt, z } from "@cyanheads/mcp-ts-core";
|
|
154
|
+
|
|
155
|
+
export const analyzeTrialLandscape = prompt("analyze_trial_landscape", {
|
|
156
|
+
description: "Guides systematic analysis of a clinical trial landscape.",
|
|
157
|
+
args: z.object({
|
|
158
|
+
topic: z
|
|
159
|
+
.string()
|
|
160
|
+
.describe("Disease, condition, or research area to analyze"),
|
|
161
|
+
focusAreas: z.array(z.string()).optional().describe("Aspects to analyze"),
|
|
162
|
+
}),
|
|
163
|
+
generate: (args) => [
|
|
164
|
+
{
|
|
165
|
+
role: "user",
|
|
166
|
+
content: {
|
|
167
|
+
type: "text",
|
|
168
|
+
text: `Analyze the trial landscape for: ${args.topic}`,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Server config
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// src/config/server-config.ts — lazy-parsed, separate from framework config
|
|
179
|
+
import { z } from "@cyanheads/mcp-ts-core";
|
|
180
|
+
import { parseEnvConfig } from "@cyanheads/mcp-ts-core/config";
|
|
181
|
+
|
|
182
|
+
const ServerConfigSchema = z.object({
|
|
183
|
+
apiBaseUrl: z
|
|
184
|
+
.string()
|
|
185
|
+
.default("https://clinicaltrials.gov/api/v2")
|
|
186
|
+
.describe("ClinicalTrials.gov API base URL"),
|
|
187
|
+
requestTimeoutMs: z.coerce
|
|
188
|
+
.number()
|
|
189
|
+
.default(30000)
|
|
190
|
+
.describe("Per-request timeout in ms"),
|
|
191
|
+
maxPageSize: z.coerce.number().default(200).describe("Maximum page size cap"),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
let _config: z.infer<typeof ServerConfigSchema> | undefined;
|
|
195
|
+
export function getServerConfig() {
|
|
196
|
+
_config ??= parseEnvConfig(ServerConfigSchema, {
|
|
197
|
+
apiBaseUrl: "CT_API_BASE_URL",
|
|
198
|
+
requestTimeoutMs: "CT_REQUEST_TIMEOUT_MS",
|
|
199
|
+
maxPageSize: "CT_MAX_PAGE_SIZE",
|
|
200
|
+
});
|
|
201
|
+
return _config;
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`parseEnvConfig` maps Zod schema paths → env var names so validation errors name the actual variable (`CT_API_BASE_URL`) rather than the internal path (`apiBaseUrl`).
|
|
206
|
+
|
|
207
|
+
### Session posture and shutdown
|
|
208
|
+
|
|
209
|
+
`src/index.ts` declares `createApp({ sessionMode: 'stateless' })`, so every launch path — Docker, `bunx`, source — serves stateless HTTP unless `MCP_SESSION_MODE` overrides it. No tool calls `ctx.requestInput`, so nothing needs `stateful`. No service holds a watcher, socket, or ref'd timer, so there is no `teardown` hook; add one alongside `setup()` if that changes.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Context
|
|
214
|
+
|
|
215
|
+
Handlers receive a unified `ctx` object. Key properties:
|
|
216
|
+
|
|
217
|
+
| Property | Description |
|
|
218
|
+
| :-------------- | :---------------------------------------------------------------------------------------------------------------------------------- |
|
|
219
|
+
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
|
|
220
|
+
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
221
|
+
| `ctx.requestId` | Unique request ID. |
|
|
222
|
+
|
|
223
|
+
Note: `ctx.state` is available but unused — this is a stateless read-only server.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Errors
|
|
228
|
+
|
|
229
|
+
Handlers throw — the framework catches, classifies, and formats.
|
|
230
|
+
|
|
231
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. The `recovery` field is required descriptive metadata (≥ 5 words, lint-validated); for the wire payload's `data.recovery.hint` (which the framework mirrors into `content[]` text unless the message already contains it verbatim), spread `ctx.recoveryFor('reason')` for the contract default, or pass `{ recovery: { hint: '...' } }` explicitly when dynamic context matters. Forwarding it is lint-enforced per throw site (`error-contract-recovery-unforwarded`). Most reasons here are raised in `ClinicalTrialsService` (a factory error carrying `data.reason` and a recovery hint), not by the handler — mark those entries `thrownBy: 'service'` so `error-contract-unthrown`, which reads only the handler body, skips them; it is lint-only metadata. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { JsonRpcErrorCode } from "@cyanheads/mcp-ts-core/errors";
|
|
235
|
+
|
|
236
|
+
errors: [
|
|
237
|
+
{ reason: "path_not_found", code: JsonRpcErrorCode.NotFound,
|
|
238
|
+
when: "Field path doesn't match the data model tree",
|
|
239
|
+
recovery: "Call clinicaltrials_get_field_definitions with no path to see top-level sections." },
|
|
240
|
+
],
|
|
241
|
+
async handler(input, ctx) {
|
|
242
|
+
const node = navigateToPath(tree, input.path);
|
|
243
|
+
if (!node) throw ctx.fail("path_not_found", `Path '${input.path}' not found`);
|
|
244
|
+
return { node };
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Declare contracts inline on each tool, even when similar across tools.** The contract is part of the tool's documented public surface — reading one tool definition file should give the full picture. Don't extract a shared `errors[]` constant or contract module to deduplicate; per-tool repetition is the intended cost of locality.
|
|
249
|
+
|
|
250
|
+
**Fallback (no contract entry fits):** factories or plain `Error`.
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
// Error factories — explicit code, concise
|
|
254
|
+
import { notFound, serviceUnavailable } from "@cyanheads/mcp-ts-core/errors";
|
|
255
|
+
throw notFound("Study not found", { nctId });
|
|
256
|
+
throw serviceUnavailable(
|
|
257
|
+
"ClinicalTrials.gov API unavailable",
|
|
258
|
+
{ url },
|
|
259
|
+
{ cause: err },
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Plain Error — framework auto-classifies from message patterns
|
|
263
|
+
throw new Error("Study not found"); // → NotFound
|
|
264
|
+
|
|
265
|
+
// HTTP errors from upstream — use httpErrorFromResponse for status-aware classification
|
|
266
|
+
import { httpErrorFromResponse } from "@cyanheads/mcp-ts-core/utils";
|
|
267
|
+
throw await httpErrorFromResponse(res, { service: "ClinicalTrials.gov" });
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
See framework CLAUDE.md and the `api-errors` skill for the full auto-classification table, all available factories, and the contract reference.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Structure
|
|
275
|
+
|
|
276
|
+
```text
|
|
277
|
+
src/
|
|
278
|
+
index.ts # createApp() entry point
|
|
279
|
+
config/
|
|
280
|
+
server-config.ts # CT_* env vars (Zod schema)
|
|
281
|
+
services/
|
|
282
|
+
clinical-trials/
|
|
283
|
+
clinical-trials-service.ts # API client (init/accessor pattern)
|
|
284
|
+
types.ts # Study, PagedStudies, FieldValueStats, FieldNode types
|
|
285
|
+
mcp-server/
|
|
286
|
+
tools/definitions/
|
|
287
|
+
search-studies.tool.ts # clinicaltrials_search_studies
|
|
288
|
+
get-study.tool.ts # clinicaltrials_get_study
|
|
289
|
+
get-study-results.tool.ts # clinicaltrials_get_study_results
|
|
290
|
+
get-field-values.tool.ts # clinicaltrials_get_field_values
|
|
291
|
+
get-field-definitions.tool.ts # clinicaltrials_get_field_definitions
|
|
292
|
+
get-study-count.tool.ts # clinicaltrials_get_study_count
|
|
293
|
+
find-eligible.tool.ts # clinicaltrials_find_eligible
|
|
294
|
+
index.ts # allToolDefinitions barrel
|
|
295
|
+
tools/utils/
|
|
296
|
+
query-helpers.ts # toArray, buildAdvancedFilter shared helpers
|
|
297
|
+
resources/definitions/
|
|
298
|
+
study.resource.ts # clinicaltrials://{nctId}
|
|
299
|
+
index.ts # allResourceDefinitions barrel
|
|
300
|
+
prompts/definitions/
|
|
301
|
+
analyze-trial-landscape.prompt.ts # analyze_trial_landscape
|
|
302
|
+
index.ts # allPromptDefinitions barrel
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Naming
|
|
308
|
+
|
|
309
|
+
| What | Convention | Example |
|
|
310
|
+
| :------------------------- | :------------------------------------------------------ | :------------------------------------- |
|
|
311
|
+
| Files | kebab-case with suffix | `search-studies.tool.ts` |
|
|
312
|
+
| Tool/resource/prompt names | snake*case with `clinicaltrials*` prefix | `clinicaltrials_search_studies` |
|
|
313
|
+
| Directories | kebab-case | `src/services/clinical-trials/` |
|
|
314
|
+
| Descriptions | Single string or template literal, no `+` concatenation | `'Search for clinical trial studies.'` |
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Skills
|
|
319
|
+
|
|
320
|
+
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.
|
|
321
|
+
|
|
322
|
+
**Agent skill directory:** Claude Code discovers skills at `.claude/skills/`. The `maintenance` skill re-syncs this directory from `framework-skills/` automatically (Phase B) after framework updates.
|
|
323
|
+
|
|
324
|
+
Available skills:
|
|
325
|
+
|
|
326
|
+
| Skill | Purpose |
|
|
327
|
+
| :----------------------- | :----------------------------------------------------------------------------------------- |
|
|
328
|
+
| `setup` | Post-init project orientation |
|
|
329
|
+
| `design-mcp-server` | Design tool surface, resources, and services for a new server |
|
|
330
|
+
| `add-tool` | Scaffold a new tool definition |
|
|
331
|
+
| `add-app-tool` | Scaffold an MCP App tool + paired UI resource |
|
|
332
|
+
| `add-resource` | Scaffold a new resource definition |
|
|
333
|
+
| `add-prompt` | Scaffold a new prompt definition |
|
|
334
|
+
| `add-service` | Scaffold a new service integration |
|
|
335
|
+
| `add-test` | Scaffold test file for a tool, resource, or service |
|
|
336
|
+
| `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
|
|
337
|
+
| `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
|
|
338
|
+
| `tool-defs-analysis` | Audit definition language across the surface (voice, leaks, recovery, cross-refs) |
|
|
339
|
+
| `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
|
|
340
|
+
| `devcheck` | Lint, format, typecheck, audit |
|
|
341
|
+
| `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
|
|
342
|
+
| `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
|
|
343
|
+
| `git-wrapup` | Land working-tree changes as a commit stack — version bump, changelog, verify, commit by concern, release commit on top. No tag, no push to `main`; halts at the open release PR |
|
|
344
|
+
| `release-pr-review` | Review pass on the open release PR — simplifier + correctness review, fixes as ordinary commits on top of the stack, PR body kept in sync |
|
|
345
|
+
| `release-and-publish` | Fast-forwards `main`, tags, pushes, publishes to npm/MCP Registry/GH Release/GHCR. Picks up from `release-pr-review` |
|
|
346
|
+
| `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
|
|
347
|
+
| `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
|
|
348
|
+
| `api-auth` | Auth modes, scopes, JWT/OAuth |
|
|
349
|
+
| `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
|
|
350
|
+
| `api-mirror` | MirrorService: persistent SQLite-backed local mirror of a bulk upstream dataset — Tier 3 opt-in |
|
|
351
|
+
| `api-config` | AppConfig, parseConfig, env vars |
|
|
352
|
+
| `api-context` | Context interface, RequestContext, logger, state, multi-round-trip input |
|
|
353
|
+
| `api-errors` | McpError, JsonRpcErrorCode, error patterns, typed contracts |
|
|
354
|
+
| `api-linter` | Definition lint rule reference — look up rule IDs reported by `lint:mcp`/devcheck |
|
|
355
|
+
| `api-services` | LLM, Speech, Graph services |
|
|
356
|
+
| `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
|
|
357
|
+
| `api-testing` | createMockContext, test patterns |
|
|
358
|
+
| `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
|
|
359
|
+
| `api-workers` | Cloudflare Workers runtime |
|
|
360
|
+
| `techniques` | Catalog of reusable response/data-shaping patterns (outline-on-overflow, etc.) |
|
|
361
|
+
| `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
|
|
362
|
+
|
|
363
|
+
When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Commands
|
|
368
|
+
|
|
369
|
+
| Command | Purpose |
|
|
370
|
+
| :------------------------ | :------------------------------------------------------------ |
|
|
371
|
+
| `bun run build` | Compile TypeScript |
|
|
372
|
+
| `bun run rebuild` | Clean + build |
|
|
373
|
+
| `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
|
|
374
|
+
| `bun run lint:mcp` | Lint tool/resource/prompt definitions (also a devcheck step) |
|
|
375
|
+
| `bun run tree` | Generate directory structure doc |
|
|
376
|
+
| `bun run format` | Auto-fix formatting |
|
|
377
|
+
| `bun run test` | Run tests (Vitest) |
|
|
378
|
+
| `bun run start:stdio` | Production mode (stdio) |
|
|
379
|
+
| `bun run start:http` | Production mode (HTTP) |
|
|
380
|
+
| `bun run inspector` | Launch MCP Inspector |
|
|
381
|
+
| `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
|
|
382
|
+
| `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
|
|
383
|
+
| `bun run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |
|
|
384
|
+
| `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` |
|
|
385
|
+
| `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` |
|
|
386
|
+
|
|
387
|
+
**CI is one file.** `.github/workflows/codeql.yml` is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## Bundling
|
|
392
|
+
|
|
393
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
|
|
394
|
+
|
|
395
|
+
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
|
|
396
|
+
|
|
397
|
+
**README install badges** (Claude Desktop `.mcpb`, Cursor, VS Code) and the `base64` / `encodeURIComponent` config-generation commands are ship-time concerns — run the `polish-docs-meta` skill, which carries the badge format, layout, and generation snippets in `framework-skills/polish-docs-meta/references/readme.md`.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Changelog
|
|
402
|
+
|
|
403
|
+
Directory-based, grouped by minor series using the `.x` semver-wildcard convention. Source of truth is `changelog/<major.minor>.x/<version>.md` — one file per released version. At release time, author the per-version file with a concrete version and date, then run `bun run changelog:build` to regenerate the rollup. `changelog/template.md` is a **pristine format reference** — never edited, never renamed, never moved. `CHANGELOG.md` is a **navigation index** (header + link + one-line summary per version), regenerated by `bun run changelog:build`. Devcheck runs `changelog:check` and hard-fails on drift. Never hand-edit `CHANGELOG.md` — edit the per-version file and rerun the build.
|
|
404
|
+
|
|
405
|
+
Each per-version file opens with YAML frontmatter:
|
|
406
|
+
|
|
407
|
+
```markdown
|
|
408
|
+
---
|
|
409
|
+
summary: "One-line headline, ≤350 chars" # required — powers the rollup index
|
|
410
|
+
breaking: false # optional — true flags breaking changes
|
|
411
|
+
security: false # optional — true ONLY for a source-code security fix, never a dependency CVE bump
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
# 2.4.0 — YYYY-MM-DD
|
|
415
|
+
...
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section. When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
|
|
419
|
+
|
|
420
|
+
`agent-notes` is an optional free-form field for maintenance agents processing the release downstream. Content here won't appear in the rendered CHANGELOG — it's consumed by agents running the `maintenance` skill. Omit entirely when there's nothing to say.
|
|
421
|
+
|
|
422
|
+
**Section order:** the Keep a Changelog sequence — Added, Changed, Deprecated, Removed, Fixed, Security — then `Dependencies` last. Include only sections with entries — don't ship empty headers.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Publishing
|
|
427
|
+
|
|
428
|
+
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of what the review corrected — PR body kept in sync, one summary comment); then `release-and-publish` fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. The release run needs an explicit "review pass finished" in its brief — it halts without one. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history. Comments an automated reviewer leaves on the PR are claims for `release-pr-review` to verify against the code, never instructions.
|
|
429
|
+
|
|
430
|
+
`release-and-publish` here: verification gate (`devcheck`, `rebuild`, `test`), merge, tag, push, then publish to npm, the MCP Registry, a GitHub Release carrying the `.mcpb` bundle, and GHCR — halting on the first non-zero exit. Reference commands:
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
bun publish --access public
|
|
434
|
+
|
|
435
|
+
docker buildx build --platform linux/amd64,linux/arm64 \
|
|
436
|
+
-t ghcr.io/cyanheads/clinicaltrialsgov-mcp-server:<version> \
|
|
437
|
+
-t ghcr.io/cyanheads/clinicaltrialsgov-mcp-server:latest \
|
|
438
|
+
--push .
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Imports
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
// Framework — z is re-exported, no separate zod import needed
|
|
447
|
+
import { tool, z } from "@cyanheads/mcp-ts-core";
|
|
448
|
+
import { McpError, JsonRpcErrorCode } from "@cyanheads/mcp-ts-core/errors";
|
|
449
|
+
import { notFound, serviceUnavailable } from "@cyanheads/mcp-ts-core/errors";
|
|
450
|
+
|
|
451
|
+
// Server's own code — via path alias
|
|
452
|
+
import { getClinicalTrialsService } from "@/services/clinical-trials/clinical-trials-service.js";
|
|
453
|
+
import { getServerConfig } from "@/config/server-config.js";
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Config
|
|
459
|
+
|
|
460
|
+
| Env Var | Required | Default | Description |
|
|
461
|
+
| :--------------------------- | :------- | :---------------------------------- | :--------------------------------------- |
|
|
462
|
+
| `CT_API_BASE_URL` | No | `https://clinicaltrials.gov/api/v2` | API base URL override |
|
|
463
|
+
| `CT_REQUEST_TIMEOUT_MS` | No | `30000` | Per-request timeout in ms |
|
|
464
|
+
| `CT_MAX_PAGE_SIZE` | No | `200` | Maximum page size cap |
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Checklist
|
|
469
|
+
|
|
470
|
+
- [ ] Zod schemas: all fields have `.describe()`, only JSON-Schema-serializable types (no `z.custom()`, `z.date()`, `z.transform()`, `z.bigint()`, `z.symbol()`, `z.void()`, `z.map()`, `z.set()`, `z.function()`, `z.nan()`)
|
|
471
|
+
- [ ] Optional nested objects: handler guards for empty inner values from form-based clients (`if (input.obj?.field && ...)`, not just `if (input.obj)`). When regex/length constraints matter, use `z.union([z.literal(''), z.string().regex(...).describe(...)])` — literal variants are exempt from `describe-on-fields`.
|
|
472
|
+
- [ ] JSDoc `@fileoverview` + `@module` on every file
|
|
473
|
+
- [ ] `ctx.log` for request-scoped logging, no `console` calls
|
|
474
|
+
- [ ] Handlers throw on failure — error factories or plain `Error`, no try/catch
|
|
475
|
+
- [ ] `format()` renders all data the LLM needs — different clients forward different surfaces (Claude Code → `structuredContent`, Claude Desktop → `content[]`); both must carry the same data
|
|
476
|
+
- [ ] Raw/domain/output schemas reviewed against real ClinicalTrials.gov sparsity/nullability before finalizing required vs optional fields
|
|
477
|
+
- [ ] Normalization and `format()` preserve uncertainty — do not fabricate facts from missing upstream data
|
|
478
|
+
- [ ] Tests include at least one sparse payload case with omitted upstream fields
|
|
479
|
+
- [ ] Registered in `createApp()` arrays (directly or via barrel exports)
|
|
480
|
+
- [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
|
|
481
|
+
- [ ] `.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
|
|
482
|
+
- [ ] `.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
|
|
483
|
+
- [ ] `.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`
|
|
484
|
+
- [ ] `bun run devcheck` passes
|