.clinerules•9 kB
# ClinicalTrials.gov MCP Server: Developer Guide & Architectural Standards
**Effective Date:** 2025-07-26
**Version:** 2.0
## Preamble
This document mandates the development practices, architectural patterns, and operational procedures for the `clinicaltrialsgov-mcp-server`. It is the authoritative guide for ensuring code quality, consistency, and maintainability. All development must adhere to these standards.
## I. Core Architectural Principles
Our architecture is built on a clear separation of concerns, ensuring that code is modular, testable, and easy to understand.
### 1. Logic Throws, Handlers Catch
This is the immutable cornerstone of our error-handling and control-flow strategy.
- **Core Logic (`logic.ts`)**: This layer's sole responsibility is the execution of pure business logic. It must be self-contained. If an operational or validation error occurs, it **must** terminate its execution by **throwing a structured `McpError`**. Logic files shall **not** contain `try...catch` blocks for formatting a final response.
- **Handlers (`registration.ts`, Transports)**: This layer's responsibility is to interface with the MCP server, invoke core logic, and manage the final response lifecycle. It **must** wrap every call to the logic layer in a `try...catch` block. This is the **exclusive** location where errors are caught, processed by the `ErrorHandler`, and formatted into a definitive `CallToolResult`.
### 2. Structured, Traceable Operations
Every operation must be fully traceable via structured logging and context propagation.
- **`RequestContext`**: Any significant operation shall be initiated by creating a `RequestContext`. This context, containing a unique `requestId`, **must** be passed as an argument through the entire call stack.
- **`Logger`**: All logging shall be performed through the centralized `logger` singleton. Every log entry **must** include the `RequestContext`.
## II. Tool Development Workflow
This section mandates the workflow for creating and modifying all tools.
### A. File and Directory Structure
Each tool shall reside in a dedicated directory within `src/mcp-server/tools/` and follow this structure:
- **`toolName/`**
- **`index.ts`**: A barrel file that exports only the `register...` function from `registration.ts`.
- **`logic.ts`**: Contains the tool's core business logic. It **must** define and export the tool's Zod input and output schemas, all inferred TypeScript types, and the main logic function.
- **`registration.ts`**: Registers the tool with the MCP server. It imports from `logic.ts` and implements the "Handler" role.
### B. The Authoritative Pattern: `getStudy` Tool
The `clinicaltrials_get_study` tool serves as a canonical example for tool development in this project.
**Step 1: Define Schemas and Logic (`logic.ts`)**
The `logic.ts` file defines the "what" and "how" of the tool. It is self-contained and throws errors when it cannot fulfill its contract.
```typescript
/**
* @fileoverview Defines the core logic, schemas, and types for the `clinicaltrials_get_study` tool.
* @module src/mcp-server/tools/getStudy/logic
*/
import { z } from "zod";
import { logger, type RequestContext } from "../../../utils/index.js";
import { ClinicalTrialsGovService } from "../../../services/clinical-trials-gov/ClinicalTrialsGovService.js";
import { Study } from "../../../services/clinical-trials-gov/types.js";
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
// 1. DEFINE the Zod input schema.
// CRITICAL: Descriptions are sent to the LLM. They must be clear, concise,
// and contain all necessary context for the model to use the tool effectively.
export const GetStudyInputSchema = z.object({
nctId: z
.string()
.regex(/^[Nn][Cc][Tt]\d+$/)
.describe("The NCT Number of the study to fetch."),
// ... other fields
});
// 2. DEFINE the Zod response schema for structured output.
export const GetStudyOutputSchema = Study; // Assuming 'Study' is a Zod schema
// 3. INFER and export TypeScript types.
export type GetStudyInput = z.infer<typeof GetStudyInputSchema>;
export type GetStudyOutput = z.infer<typeof GetStudyOutputSchema>;
/**
* 4. IMPLEMENT the core logic function.
* It must remain pure; its only concerns are its inputs and its return value or thrown error.
* @param params The validated input parameters.
* @param context The request context for logging and tracing.
* @returns A promise resolving with the structured study data.
* @throws {McpError} If the logic encounters an unrecoverable issue.
*/
export async function getStudyLogic(
params: GetStudyInput,
context: RequestContext
): Promise<GetStudyOutput> {
logger.debug(`Fetching study ${params.nctId}...`, { ...context });
const service = new ClinicalTrialsGovService();
const study = await service.fetchStudy(params.nctId, context);
if (!study) {
throw new McpError(
BaseErrorCode.NOT_FOUND,
`Study with NCT ID '${params.nctId}' not found.`
);
}
return study;
}
```
**Step 2: Register the Tool and Handle Outcomes (`registration.ts`)**
The `registration.ts` file wires the logic into the MCP server and handles all outcomes.
```typescript
/**
* @fileoverview Handles registration and error handling for the `clinicaltrials_get_study` tool.
* @module src/mcp-server/tools/getStudy/registration
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
import { GetStudyInput, GetStudyInputSchema, getStudyLogic, GetStudyOutputSchema } from "./logic.js";
import { McpError } from "../../../types-global/errors.js";
/**
* Registers the 'clinicaltrials_get_study' tool with the MCP server.
* @param server - The MCP server instance.
*/
export const registerGetStudyTool = async (server: McpServer): Promise<void> => {
const toolName = "clinicaltrials_get_study";
server.registerTool(
toolName,
{
title: "Get Clinical Study",
description: "Retrieves detailed information for a single clinical study by its NCT number.",
inputSchema: GetStudyInputSchema.shape,
outputSchema: GetStudyOutputSchema.shape, // MANDATORY
annotations: { readOnlyHint: true },
},
async (params: GetStudyInput, callContext) => {
const handlerContext = requestContextService.createRequestContext({ toolName, parentContext: callContext });
try {
const result = await getStudyLogic(params, handlerContext);
return {
structuredContent: result,
content: [{ type: "text", text: `Successfully retrieved study ${params.nctId}.` }],
};
} catch (error) {
logger.error(`Error in ${toolName} handler`, { error, ...handlerContext });
const mcpError = ErrorHandler.handleError(error, {
operation: toolName,
context: handlerContext,
input: params,
}) as McpError;
return {
isError: true,
content: [{ type: "text", text: mcpError.message }],
structuredContent: {
code: mcpError.code,
message: mcpError.message,
details: mcpError.details,
},
};
}
}
);
logger.info(`Tool '${toolName}' registered successfully.`);
};
```
## III. Integrating External Services
For interacting with the ClinicalTrials.gov API, use the dedicated service singleton.
- **`ClinicalTrialsGovService`**: Located at `src/services/clinical-trials-gov/ClinicalTrialsGovService.ts`, this class manages all interactions with the external API. It encapsulates the base URL, handles request construction, and performs initial response validation.
- **Usage**: Import the `ClinicalTrialsGovService` into your tool's `logic.ts` file to use it.
## IV. Code Quality and Documentation
- **JSDoc**: Every file must start with a `@fileoverview` and `@module` block. All exported functions and types must have clear, concise JSDoc comments.
- **LLM-Facing Descriptions**: The tool's `title`, `description`, and all parameter descriptions in Zod schemas (`.describe()`) are transmitted directly to the LLM. They must be written with the LLM as the primary audience, being descriptive, concise, and explicit about requirements.
- **Clarity Over Brevity**: Write self-documenting code with meaningful variable and function names.
- **Formatting**: All code must be formatted using Prettier (`npm run format`) before committing.
## V. Security Mandates
- **Input Sanitization**: All inputs from external sources must be treated as untrusted and validated with Zod.
- **Secrets Management**: All secrets must be loaded from environment variables via the `config` module.
- **Authentication & Authorization**: The server supports `jwt` and `oauth` modes. Protect tools by checking scopes where necessary.
This guide is the single source of truth for development standards. All code reviews will be conducted against these principles.