Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Guides creation of high-quality MCP servers in TypeScript or Python (FastMCP) to connect LLMs with external services.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/node_mcp_server.md
1# Node/TypeScript MCP Server Implementation Guide23## Overview45This document provides Node/TypeScript-specific best practices and examples for implementing MCP servers using the MCP TypeScript SDK. It covers project structure, server setup, tool registration patterns, input validation with Zod, error handling, and complete working examples.67---89## Quick Reference1011### Key Imports12```typescript13import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";14import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";15import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";16import express from "express";17import { z } from "zod";18```1920### Server Initialization21```typescript22const server = new McpServer({23name: "service-mcp-server",24version: "1.0.0"25});26```2728### Tool Registration Pattern29```typescript30server.registerTool(31"tool_name",32{33title: "Tool Display Name",34description: "What the tool does",35inputSchema: { param: z.string() },36outputSchema: { result: z.string() }37},38async ({ param }) => {39const output = { result: `Processed: ${param}` };40return {41content: [{ type: "text", text: JSON.stringify(output) }],42structuredContent: output // Modern pattern for structured data43};44}45);46```4748---4950## MCP TypeScript SDK5152The official MCP TypeScript SDK provides:53- `McpServer` class for server initialization54- `registerTool` method for tool registration55- Zod schema integration for runtime input validation56- Type-safe tool handler implementations5758**IMPORTANT - Use Modern APIs Only:**59- **DO use**: `server.registerTool()`, `server.registerResource()`, `server.registerPrompt()`60- **DO NOT use**: Old deprecated APIs such as `server.tool()`, `server.setRequestHandler(ListToolsRequestSchema, ...)`, or manual handler registration61- The `register*` methods provide better type safety, automatic schema handling, and are the recommended approach6263See the MCP SDK documentation in the references for complete details.6465## Server Naming Convention6667Node/TypeScript MCP servers must follow this naming pattern:68- **Format**: `{service}-mcp-server` (lowercase with hyphens)69- **Examples**: `github-mcp-server`, `jira-mcp-server`, `stripe-mcp-server`7071The name should be:72- General (not tied to specific features)73- Descriptive of the service/API being integrated74- Easy to infer from the task description75- Without version numbers or dates7677## Project Structure7879Create the following structure for Node/TypeScript MCP servers:8081```82{service}-mcp-server/83├── package.json84├── tsconfig.json85├── README.md86├── src/87│ ├── index.ts # Main entry point with McpServer initialization88│ ├── types.ts # TypeScript type definitions and interfaces89│ ├── tools/ # Tool implementations (one file per domain)90│ ├── services/ # API clients and shared utilities91│ ├── schemas/ # Zod validation schemas92│ └── constants.ts # Shared constants (API_URL, CHARACTER_LIMIT, etc.)93└── dist/ # Built JavaScript files (entry point: dist/index.js)94```9596## Tool Implementation9798### Tool Naming99100Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names.101102**Avoid Naming Conflicts**: Include the service context to prevent overlaps:103- Use "slack_send_message" instead of just "send_message"104- Use "github_create_issue" instead of just "create_issue"105- Use "asana_list_tasks" instead of just "list_tasks"106107### Tool Structure108109Tools are registered using the `registerTool` method with the following requirements:110- Use Zod schemas for runtime input validation and type safety111- The `description` field must be explicitly provided - JSDoc comments are NOT automatically extracted112- Explicitly provide `title`, `description`, `inputSchema`, and `annotations`113- The `inputSchema` must be a Zod schema object (not a JSON schema)114- Type all parameters and return values explicitly115116```typescript117import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";118import { z } from "zod";119120const server = new McpServer({121name: "example-mcp",122version: "1.0.0"123});124125// Zod schema for input validation126const UserSearchInputSchema = z.object({127query: z.string()128.min(2, "Query must be at least 2 characters")129.max(200, "Query must not exceed 200 characters")130.describe("Search string to match against names/emails"),131limit: z.number()132.int()133.min(1)134.max(100)135.default(20)136.describe("Maximum results to return"),137offset: z.number()138.int()139.min(0)140.default(0)141.describe("Number of results to skip for pagination"),142response_format: z.nativeEnum(ResponseFormat)143.default(ResponseFormat.MARKDOWN)144.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")145}).strict();146147// Type definition from Zod schema148type UserSearchInput = z.infer<typeof UserSearchInputSchema>;149150server.registerTool(151"example_search_users",152{153title: "Search Example Users",154description: `Search for users in the Example system by name, email, or team.155156This tool searches across all user profiles in the Example platform, supporting partial matches and various search filters. It does NOT create or modify users, only searches existing ones.157158Args:159- query (string): Search string to match against names/emails160- limit (number): Maximum results to return, between 1-100 (default: 20)161- offset (number): Number of results to skip for pagination (default: 0)162- response_format ('markdown' | 'json'): Output format (default: 'markdown')163164Returns:165For JSON format: Structured data with schema:166{167"total": number, // Total number of matches found168"count": number, // Number of results in this response169"offset": number, // Current pagination offset170"users": [171{172"id": string, // User ID (e.g., "U123456789")173"name": string, // Full name (e.g., "John Doe")174"email": string, // Email address175"team": string, // Team name (optional)176"active": boolean // Whether user is active177}178],179"has_more": boolean, // Whether more results are available180"next_offset": number // Offset for next page (if has_more is true)181}182183Examples:184- Use when: "Find all marketing team members" -> params with query="team:marketing"185- Use when: "Search for John's account" -> params with query="john"186- Don't use when: You need to create a user (use example_create_user instead)187188Error Handling:189- Returns "Error: Rate limit exceeded" if too many requests (429 status)190- Returns "No users found matching '<query>'" if search returns empty`,191inputSchema: UserSearchInputSchema,192annotations: {193readOnlyHint: true,194destructiveHint: false,195idempotentHint: true,196openWorldHint: true197}198},199async (params: UserSearchInput) => {200try {201// Input validation is handled by Zod schema202// Make API request using validated parameters203const data = await makeApiRequest<any>(204"users/search",205"GET",206undefined,207{208q: params.query,209limit: params.limit,210offset: params.offset211}212);213214const users = data.users || [];215const total = data.total || 0;216217if (!users.length) {218return {219content: [{220type: "text",221text: `No users found matching '${params.query}'`222}]223};224}225226// Prepare structured output227const output = {228total,229count: users.length,230offset: params.offset,231users: users.map((user: any) => ({232id: user.id,233name: user.name,234email: user.email,235...(user.team ? { team: user.team } : {}),236active: user.active ?? true237})),238has_more: total > params.offset + users.length,239...(total > params.offset + users.length ? {240next_offset: params.offset + users.length241} : {})242};243244// Format text representation based on requested format245let textContent: string;246if (params.response_format === ResponseFormat.MARKDOWN) {247const lines = [`# User Search Results: '${params.query}'`, "",248`Found ${total} users (showing ${users.length})`, ""];249for (const user of users) {250lines.push(`## ${user.name} (${user.id})`);251lines.push(`- **Email**: ${user.email}`);252if (user.team) lines.push(`- **Team**: ${user.team}`);253lines.push("");254}255textContent = lines.join("\n");256} else {257textContent = JSON.stringify(output, null, 2);258}259260return {261content: [{ type: "text", text: textContent }],262structuredContent: output // Modern pattern for structured data263};264} catch (error) {265return {266content: [{267type: "text",268text: handleApiError(error)269}]270};271}272}273);274```275276## Zod Schemas for Input Validation277278Zod provides runtime type validation:279280```typescript281import { z } from "zod";282283// Basic schema with validation284const CreateUserSchema = z.object({285name: z.string()286.min(1, "Name is required")287.max(100, "Name must not exceed 100 characters"),288email: z.string()289.email("Invalid email format"),290age: z.number()291.int("Age must be a whole number")292.min(0, "Age cannot be negative")293.max(150, "Age cannot be greater than 150")294}).strict(); // Use .strict() to forbid extra fields295296// Enums297enum ResponseFormat {298MARKDOWN = "markdown",299JSON = "json"300}301302const SearchSchema = z.object({303response_format: z.nativeEnum(ResponseFormat)304.default(ResponseFormat.MARKDOWN)305.describe("Output format")306});307308// Optional fields with defaults309const PaginationSchema = z.object({310limit: z.number()311.int()312.min(1)313.max(100)314.default(20)315.describe("Maximum results to return"),316offset: z.number()317.int()318.min(0)319.default(0)320.describe("Number of results to skip")321});322```323324## Response Format Options325326Support multiple output formats for flexibility:327328```typescript329enum ResponseFormat {330MARKDOWN = "markdown",331JSON = "json"332}333334const inputSchema = z.object({335query: z.string(),336response_format: z.nativeEnum(ResponseFormat)337.default(ResponseFormat.MARKDOWN)338.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")339});340```341342**Markdown format**:343- Use headers, lists, and formatting for clarity344- Convert timestamps to human-readable format345- Show display names with IDs in parentheses346- Omit verbose metadata347- Group related information logically348349**JSON format**:350- Return complete, structured data suitable for programmatic processing351- Include all available fields and metadata352- Use consistent field names and types353354## Pagination Implementation355356For tools that list resources:357358```typescript359const ListSchema = z.object({360limit: z.number().int().min(1).max(100).default(20),361offset: z.number().int().min(0).default(0)362});363364async function listItems(params: z.infer<typeof ListSchema>) {365const data = await apiRequest(params.limit, params.offset);366367const response = {368total: data.total,369count: data.items.length,370offset: params.offset,371items: data.items,372has_more: data.total > params.offset + data.items.length,373next_offset: data.total > params.offset + data.items.length374? params.offset + data.items.length375: undefined376};377378return JSON.stringify(response, null, 2);379}380```381382## Character Limits and Truncation383384Add a CHARACTER_LIMIT constant to prevent overwhelming responses:385386```typescript387// At module level in constants.ts388export const CHARACTER_LIMIT = 25000; // Maximum response size in characters389390async function searchTool(params: SearchInput) {391let result = generateResponse(data);392393// Check character limit and truncate if needed394if (result.length > CHARACTER_LIMIT) {395const truncatedData = data.slice(0, Math.max(1, data.length / 2));396response.data = truncatedData;397response.truncated = true;398response.truncation_message =399`Response truncated from ${data.length} to ${truncatedData.length} items. ` +400`Use 'offset' parameter or add filters to see more results.`;401result = JSON.stringify(response, null, 2);402}403404return result;405}406```407408## Error Handling409410Provide clear, actionable error messages:411412```typescript413import axios, { AxiosError } from "axios";414415function handleApiError(error: unknown): string {416if (error instanceof AxiosError) {417if (error.response) {418switch (error.response.status) {419case 404:420return "Error: Resource not found. Please check the ID is correct.";421case 403:422return "Error: Permission denied. You don't have access to this resource.";423case 429:424return "Error: Rate limit exceeded. Please wait before making more requests.";425default:426return `Error: API request failed with status ${error.response.status}`;427}428} else if (error.code === "ECONNABORTED") {429return "Error: Request timed out. Please try again.";430}431}432return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`;433}434```435436## Shared Utilities437438Extract common functionality into reusable functions:439440```typescript441// Shared API request function442async function makeApiRequest<T>(443endpoint: string,444method: "GET" | "POST" | "PUT" | "DELETE" = "GET",445data?: any,446params?: any447): Promise<T> {448try {449const response = await axios({450method,451url: `${API_BASE_URL}/${endpoint}`,452data,453params,454timeout: 30000,455headers: {456"Content-Type": "application/json",457"Accept": "application/json"458}459});460return response.data;461} catch (error) {462throw error;463}464}465```466467## Async/Await Best Practices468469Always use async/await for network requests and I/O operations:470471```typescript472// Good: Async network request473async function fetchData(resourceId: string): Promise<ResourceData> {474const response = await axios.get(`${API_URL}/resource/${resourceId}`);475return response.data;476}477478// Bad: Promise chains479function fetchData(resourceId: string): Promise<ResourceData> {480return axios.get(`${API_URL}/resource/${resourceId}`)481.then(response => response.data); // Harder to read and maintain482}483```484485## TypeScript Best Practices4864871. **Use Strict TypeScript**: Enable strict mode in tsconfig.json4882. **Define Interfaces**: Create clear interface definitions for all data structures4893. **Avoid `any`**: Use proper types or `unknown` instead of `any`4904. **Zod for Runtime Validation**: Use Zod schemas to validate external data4915. **Type Guards**: Create type guard functions for complex type checking4926. **Error Handling**: Always use try-catch with proper error type checking4937. **Null Safety**: Use optional chaining (`?.`) and nullish coalescing (`??`)494495```typescript496// Good: Type-safe with Zod and interfaces497interface UserResponse {498id: string;499name: string;500email: string;501team?: string;502active: boolean;503}504505const UserSchema = z.object({506id: z.string(),507name: z.string(),508email: z.string().email(),509team: z.string().optional(),510active: z.boolean()511});512513type User = z.infer<typeof UserSchema>;514515async function getUser(id: string): Promise<User> {516const data = await apiCall(`/users/${id}`);517return UserSchema.parse(data); // Runtime validation518}519520// Bad: Using any521async function getUser(id: string): Promise<any> {522return await apiCall(`/users/${id}`); // No type safety523}524```525526## Package Configuration527528### package.json529530```json531{532"name": "{service}-mcp-server",533"version": "1.0.0",534"description": "MCP server for {Service} API integration",535"type": "module",536"main": "dist/index.js",537"scripts": {538"start": "node dist/index.js",539"dev": "tsx watch src/index.ts",540"build": "tsc",541"clean": "rm -rf dist"542},543"engines": {544"node": ">=18"545},546"dependencies": {547"@modelcontextprotocol/sdk": "^1.6.1",548"axios": "^1.7.9",549"zod": "^3.23.8"550},551"devDependencies": {552"@types/node": "^22.10.0",553"tsx": "^4.19.2",554"typescript": "^5.7.2"555}556}557```558559### tsconfig.json560561```json562{563"compilerOptions": {564"target": "ES2022",565"module": "Node16",566"moduleResolution": "Node16",567"lib": ["ES2022"],568"outDir": "./dist",569"rootDir": "./src",570"strict": true,571"esModuleInterop": true,572"skipLibCheck": true,573"forceConsistentCasingInFileNames": true,574"declaration": true,575"declarationMap": true,576"sourceMap": true,577"allowSyntheticDefaultImports": true578},579"include": ["src/**/*"],580"exclude": ["node_modules", "dist"]581}582```583584## Complete Example585586```typescript587#!/usr/bin/env node588/**589* MCP Server for Example Service.590*591* This server provides tools to interact with Example API, including user search,592* project management, and data export capabilities.593*/594595import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";596import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";597import { z } from "zod";598import axios, { AxiosError } from "axios";599600// Constants601const API_BASE_URL = "https://api.example.com/v1";602const CHARACTER_LIMIT = 25000;603604// Enums605enum ResponseFormat {606MARKDOWN = "markdown",607JSON = "json"608}609610// Zod schemas611const UserSearchInputSchema = z.object({612query: z.string()613.min(2, "Query must be at least 2 characters")614.max(200, "Query must not exceed 200 characters")615.describe("Search string to match against names/emails"),616limit: z.number()617.int()618.min(1)619.max(100)620.default(20)621.describe("Maximum results to return"),622offset: z.number()623.int()624.min(0)625.default(0)626.describe("Number of results to skip for pagination"),627response_format: z.nativeEnum(ResponseFormat)628.default(ResponseFormat.MARKDOWN)629.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")630}).strict();631632type UserSearchInput = z.infer<typeof UserSearchInputSchema>;633634// Shared utility functions635async function makeApiRequest<T>(636endpoint: string,637method: "GET" | "POST" | "PUT" | "DELETE" = "GET",638data?: any,639params?: any640): Promise<T> {641try {642const response = await axios({643method,644url: `${API_BASE_URL}/${endpoint}`,645data,646params,647timeout: 30000,648headers: {649"Content-Type": "application/json",650"Accept": "application/json"651}652});653return response.data;654} catch (error) {655throw error;656}657}658659function handleApiError(error: unknown): string {660if (error instanceof AxiosError) {661if (error.response) {662switch (error.response.status) {663case 404:664return "Error: Resource not found. Please check the ID is correct.";665case 403:666return "Error: Permission denied. You don't have access to this resource.";667case 429:668return "Error: Rate limit exceeded. Please wait before making more requests.";669default:670return `Error: API request failed with status ${error.response.status}`;671}672} else if (error.code === "ECONNABORTED") {673return "Error: Request timed out. Please try again.";674}675}676return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`;677}678679// Create MCP server instance680const server = new McpServer({681name: "example-mcp",682version: "1.0.0"683});684685// Register tools686server.registerTool(687"example_search_users",688{689title: "Search Example Users",690description: `[Full description as shown above]`,691inputSchema: UserSearchInputSchema,692annotations: {693readOnlyHint: true,694destructiveHint: false,695idempotentHint: true,696openWorldHint: true697}698},699async (params: UserSearchInput) => {700// Implementation as shown above701}702);703704// Main function705// For stdio (local):706async function runStdio() {707if (!process.env.EXAMPLE_API_KEY) {708console.error("ERROR: EXAMPLE_API_KEY environment variable is required");709process.exit(1);710}711712const transport = new StdioServerTransport();713await server.connect(transport);714console.error("MCP server running via stdio");715}716717// For streamable HTTP (remote):718async function runHTTP() {719if (!process.env.EXAMPLE_API_KEY) {720console.error("ERROR: EXAMPLE_API_KEY environment variable is required");721process.exit(1);722}723724const app = express();725app.use(express.json());726727app.post('/mcp', async (req, res) => {728const transport = new StreamableHTTPServerTransport({729sessionIdGenerator: undefined,730enableJsonResponse: true731});732res.on('close', () => transport.close());733await server.connect(transport);734await transport.handleRequest(req, res, req.body);735});736737const port = parseInt(process.env.PORT || '3000');738app.listen(port, () => {739console.error(`MCP server running on http://localhost:${port}/mcp`);740});741}742743// Choose transport based on environment744const transport = process.env.TRANSPORT || 'stdio';745if (transport === 'http') {746runHTTP().catch(error => {747console.error("Server error:", error);748process.exit(1);749});750} else {751runStdio().catch(error => {752console.error("Server error:", error);753process.exit(1);754});755}756```757758---759760## Advanced MCP Features761762### Resource Registration763764Expose data as resources for efficient, URI-based access:765766```typescript767import { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js";768769// Register a resource with URI template770server.registerResource(771{772uri: "file://documents/{name}",773name: "Document Resource",774description: "Access documents by name",775mimeType: "text/plain"776},777async (uri: string) => {778// Extract parameter from URI779const match = uri.match(/^file:\/\/documents\/(.+)$/);780if (!match) {781throw new Error("Invalid URI format");782}783784const documentName = match[1];785const content = await loadDocument(documentName);786787return {788contents: [{789uri,790mimeType: "text/plain",791text: content792}]793};794}795);796797// List available resources dynamically798server.registerResourceList(async () => {799const documents = await getAvailableDocuments();800return {801resources: documents.map(doc => ({802uri: `file://documents/${doc.name}`,803name: doc.name,804mimeType: "text/plain",805description: doc.description806}))807};808});809```810811**When to use Resources vs Tools:**812- **Resources**: For data access with simple URI-based parameters813- **Tools**: For complex operations requiring validation and business logic814- **Resources**: When data is relatively static or template-based815- **Tools**: When operations have side effects or complex workflows816817### Transport Options818819The TypeScript SDK supports two main transport mechanisms:820821#### Streamable HTTP (Recommended for Remote Servers)822823```typescript824import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";825import express from "express";826827const app = express();828app.use(express.json());829830app.post('/mcp', async (req, res) => {831// Create new transport for each request (stateless, prevents request ID collisions)832const transport = new StreamableHTTPServerTransport({833sessionIdGenerator: undefined,834enableJsonResponse: true835});836837res.on('close', () => transport.close());838839await server.connect(transport);840await transport.handleRequest(req, res, req.body);841});842843app.listen(3000);844```845846#### stdio (For Local Integrations)847848```typescript849import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";850851const transport = new StdioServerTransport();852await server.connect(transport);853```854855**Transport selection:**856- **Streamable HTTP**: Web services, remote access, multiple clients857- **stdio**: Command-line tools, local development, subprocess integration858859### Notification Support860861Notify clients when server state changes:862863```typescript864// Notify when tools list changes865server.notification({866method: "notifications/tools/list_changed"867});868869// Notify when resources change870server.notification({871method: "notifications/resources/list_changed"872});873```874875Use notifications sparingly - only when server capabilities genuinely change.876877---878879## Code Best Practices880881### Code Composability and Reusability882883Your implementation MUST prioritize composability and code reuse:8848851. **Extract Common Functionality**:886- Create reusable helper functions for operations used across multiple tools887- Build shared API clients for HTTP requests instead of duplicating code888- Centralize error handling logic in utility functions889- Extract business logic into dedicated functions that can be composed890- Extract shared markdown or JSON field selection & formatting functionality8918922. **Avoid Duplication**:893- NEVER copy-paste similar code between tools894- If you find yourself writing similar logic twice, extract it into a function895- Common operations like pagination, filtering, field selection, and formatting should be shared896- Authentication/authorization logic should be centralized897898## Building and Running899900Always build your TypeScript code before running:901902```bash903# Build the project904npm run build905906# Run the server907npm start908909# Development with auto-reload910npm run dev911```912913Always ensure `npm run build` completes successfully before considering the implementation complete.914915## Quality Checklist916917Before finalizing your Node/TypeScript MCP server implementation, ensure:918919### Strategic Design920- [ ] Tools enable complete workflows, not just API endpoint wrappers921- [ ] Tool names reflect natural task subdivisions922- [ ] Response formats optimize for agent context efficiency923- [ ] Human-readable identifiers used where appropriate924- [ ] Error messages guide agents toward correct usage925926### Implementation Quality927- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented928- [ ] All tools registered using `registerTool` with complete configuration929- [ ] All tools include `title`, `description`, `inputSchema`, and `annotations`930- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)931- [ ] All tools use Zod schemas for runtime input validation with `.strict()` enforcement932- [ ] All Zod schemas have proper constraints and descriptive error messages933- [ ] All tools have comprehensive descriptions with explicit input/output types934- [ ] Descriptions include return value examples and complete schema documentation935- [ ] Error messages are clear, actionable, and educational936937### TypeScript Quality938- [ ] TypeScript interfaces are defined for all data structures939- [ ] Strict TypeScript is enabled in tsconfig.json940- [ ] No use of `any` type - use `unknown` or proper types instead941- [ ] All async functions have explicit Promise<T> return types942- [ ] Error handling uses proper type guards (e.g., `axios.isAxiosError`, `z.ZodError`)943944### Advanced Features (where applicable)945- [ ] Resources registered for appropriate data endpoints946- [ ] Appropriate transport configured (stdio or streamable HTTP)947- [ ] Notifications implemented for dynamic server capabilities948- [ ] Type-safe with SDK interfaces949950### Project Configuration951- [ ] Package.json includes all necessary dependencies952- [ ] Build script produces working JavaScript in dist/ directory953- [ ] Main entry point is properly configured as dist/index.js954- [ ] Server name follows format: `{service}-mcp-server`955- [ ] tsconfig.json properly configured with strict mode956957### Code Quality958- [ ] Pagination is properly implemented where applicable959- [ ] Large responses check CHARACTER_LIMIT constant and truncate with clear messages960- [ ] Filtering options are provided for potentially large result sets961- [ ] All network operations handle timeouts and connection errors gracefully962- [ ] Common functionality is extracted into reusable functions963- [ ] Return types are consistent across similar operations964965### Testing and Build966- [ ] `npm run build` completes successfully without errors967- [ ] dist/index.js created and executable968- [ ] Server runs: `node dist/index.js --help`969- [ ] All imports resolve correctly970- [ ] Sample tool calls work as expected