# pubchem-mcp-server: Developer Guide & Architectural Standards
**Effective Date:** 2025-06-29
This document mandates the development practices, architectural patterns, and operational procedures for the `pubchem-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 cornerstone of our error-handling strategy.
- **Core Logic (`logic.ts`)**: This layer is responsible for business logic only. It should be pure and self-contained. If an error occurs (e.g., failed validation, API error), it **must `throw` a structured `McpError`**. Logic files **must not** contain `try...catch` blocks for formatting final responses.
- **Handlers (`registration.ts`, Transports)**: This layer is responsible for invoking core logic and managing communication protocols. It **must** wrap all calls to the logic layer in a `try...catch` block. This is the only place where errors are caught, processed by the `ErrorHandler`, and formatted into a final `CallToolResult` or HTTP response.
### 2. Structured, Traceable Operations
Every operation must be traceable from start to finish through structured logging and context propagation.
- **`RequestContext`**: Every significant operation must begin by creating a `RequestContext` using `requestContextService.createRequestContext()`. This context, containing a unique `requestId`, must be passed down through all subsequent function calls.
- **`Logger`**: All logging must be done through the centralized `logger` singleton, and every log call must include the current `RequestContext`.
## II. Tool Development Workflow
This section defines the mandatory workflow for creating and modifying tools.
### A. File and Directory Structure
Each tool must reside in its own 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 core business logic. It **must** define and export the tool's Zod input schema, all inferred TypeScript types (input and output), and the main logic function.
- **`registration.ts`**: Registers the tool with the MCP server. It imports from `logic.ts` and implements the "Handler" role described in our core principles.
### B. The Authoritative Pattern: `searchCompoundByIdentifier`
The `pubchem_search_compound_by_identifier` tool serves as the canonical example for all tool development.
**Step 1: Define Schema 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 `pubchem_search_compound_by_identifier` tool.
* @module src/mcp-server/tools/searchCompoundByIdentifier/logic
*/
import { z } from "zod";
import { pubChemApiClient } from "../../../services/pubchem/pubchemApiClient.js";
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
import { logger, type RequestContext } from "../../../utils/index.js";
// 1. Define and export the Zod schema
export const PubchemSearchCompoundByIdentifierInputSchema = z.object({
identifierType: z
.enum(["name", "smiles", "inchikey"])
.describe("The type of chemical identifier being provided."),
identifier: z
.string()
.min(1, "Identifier cannot be empty.")
.describe("The identifier string."),
});
// 2. Define and export TypeScript types
export type PubchemSearchCompoundByIdentifierInput = z.infer<
typeof PubchemSearchCompoundByIdentifierInputSchema
>;
export interface PubchemSearchCompoundByIdentifierOutput {
cids: number[];
}
/**
* 3. Implement and export the core logic
* @param params - The validated input parameters.
* @param context - The request context for logging and tracing.
* @returns A promise that resolves with the search results.
* @throws {McpError} If the API request fails or the identifier is not found.
*/
export async function pubchemSearchCompoundByIdentifierLogic(
params: PubchemSearchCompoundByIdentifierInput,
context: RequestContext
): Promise<PubchemSearchCompoundByIdentifierOutput> {
logger.debug("Processing pubchem_search_compound_by_identifier logic...", {
...context,
});
const { identifierType, identifier } = params;
const path = `/compound/${identifierType}/${encodeURIComponent(identifier)}/cids/JSON`;
const response = await pubChemApiClient.get(path, context);
// ... error handling and data transformation ...
return { cids: response.IdentifierList.CID };
}
```
**Step 2: Register the Tool and Handle Errors (`registration.ts`)**
The `registration.ts` file wires the logic into the MCP server and handles all outcomes.
```typescript
/**
* @fileoverview Handles the registration of the `pubchem_search_compound_by_identifier` tool.
* @module src/mcp-server/tools/searchCompoundByIdentifier/registration
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import {
ErrorHandler,
logger,
requestContextService,
} from "../../../utils/index.js";
import {
pubchemSearchCompoundByIdentifierLogic,
PubchemSearchCompoundByIdentifierInputSchema,
PubchemSearchCompoundByIdentifierInput,
} from "./logic.js";
export const registerPubchemSearchCompoundByIdentifierTool = async (
server: McpServer
): Promise<void> => {
const toolName = "pubchem_search_compound_by_identifier";
const toolDescription =
"Searches for PubChem Compound IDs (CIDs) using a common chemical identifier.";
server.tool(
toolName,
toolDescription,
PubchemSearchCompoundByIdentifierInputSchema.shape,
async (
params: PubchemSearchCompoundByIdentifierInput,
mcpContext: any
): Promise<CallToolResult> => {
const handlerContext = requestContextService.createRequestContext({
/* ... */
});
try {
const result = await pubchemSearchCompoundByIdentifierLogic(
params,
handlerContext
);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
isError: false,
};
} catch (error) {
const mcpError = ErrorHandler.handleError(error, {
/* ... */
});
return {
content: [
{
type: "text",
text: JSON.stringify({ error: mcpError.toJSON() }),
},
],
isError: true,
};
}
}
);
};
```
## III. Integrating External Services
For interacting with the PubChem API, the server uses a dedicated singleton client.
- **`PubChemApiClient`**: Located at `src/services/pubchem/pubchemApiClient.ts`, this singleton class manages all interactions with the PubChem PUG REST API. It handles URL construction, rate limiting (5 requests/second), request execution, and centralized error handling.
- **Usage**: Import the `pubChemApiClient` singleton instance into your tool's `logic.ts` file to make API calls.
## 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 explaining their purpose.
- **Clarity Over Brevity**: Write self-documenting code with meaningful variable and function names.
- **Immutability**: Prefer functional approaches and immutable data structures where possible to avoid side effects.
- **Formatting**: All code must be formatted using Prettier (`npm run format`) before committing.
## V. Security Mandates
- **Input Sanitization**: All inputs from external sources (tool arguments, API responses) must be treated as untrusted. Use the `sanitization` utilities where appropriate.
- **Secrets Management**: All secrets (API keys, auth keys) **must** be loaded from environment variables via the `config` module. Never hardcode secrets.
- **Authentication & Authorization**:
- The server supports `jwt` (dev) and `oauth` (prod) modes via `MCP_AUTH_MODE`.
- Protect tools by checking scopes. Use the `withRequiredScopes(["scope:read"])` utility inside your tool handler.
This guide is the single source of truth for development standards. All code reviews will be conducted against these principles.