MCP Linear App
by zalab-inc
Verified
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z, ZodRawShape } from "zod";
// Import RequestHandlerExtra and CallToolResult from the correct locations
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
/**
* Standard tool response format
*/
export type ToolResponse = CallToolResult;
/**
* Defines the raw handler function that users will implement
* No need for try-catch blocks as error handling is done automatically
*/
export type RawToolHandler<T = { [key: string]: unknown }> = (
args: T,
extra: RequestHandlerExtra
) => Promise<CallToolResult | string> | CallToolResult | string;
/**
* Defines the structure of a tool that can be registered with the MCP server
*/
export interface ToolDefinition {
name: string;
description: string;
schema: ZodRawShape;
handler: (args: { [x: string]: unknown }, extra: RequestHandlerExtra) => Promise<CallToolResult>;
}
/**
* A tool definition with automatic error handling
*/
export interface SafeToolDefinition<T extends ZodRawShape> {
name: string;
description: string;
schema: T;
handler: RawToolHandler<z.infer<z.ZodObject<T>>>;
}
/**
* Creates a safe tool with automatic error handling
* @param toolDef - The tool definition with a simpler handler
* @returns A complete ToolDefinition with error handling
*/
export function createSafeTool<T extends ZodRawShape>(
toolDef: SafeToolDefinition<T>
): ToolDefinition {
// Create a Zod object from the schema
const zodSchema = z.object(toolDef.schema);
// Return the tool definition with wrapped handler that includes error handling
return {
name: toolDef.name,
description: toolDef.description,
schema: toolDef.schema,
handler: async (args: { [x: string]: unknown }, extra: RequestHandlerExtra): Promise<CallToolResult> => {
try {
// First, validate and parse the input arguments
const validatedArgs = zodSchema.parse(args);
// Execute the handler and handle various return types
let result = await toolDef.handler(validatedArgs, extra);
// If the result is a string, convert it to a proper ToolResponse
if (typeof result === 'string') {
result = createTextResponse(result);
}
return result;
} catch (error) {
// Handle Zod validation errors specifically
if (error instanceof z.ZodError) {
return {
content: [{
type: "text",
text: `Validation error: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`
}],
isError: true,
_meta: {
errorType: 'ValidationError',
validationErrors: error.errors,
timestamp: new Date().toISOString()
}
};
}
// Handle any other errors
return {
content: [{
type: "text",
text: `Error in tool ${toolDef.name}: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true,
_meta: {
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
timestamp: new Date().toISOString(),
stack: error instanceof Error ? error.stack : undefined
}
};
}
}
};
}
/**
* Registers one or multiple tools with the MCP server
* @param server - The MCP server instance
* @param tools - A single tool definition or an array of tool definitions
*/
export function registerTool(
server: McpServer,
tools: ToolDefinition | ToolDefinition[]
): void {
const toolArray = Array.isArray(tools) ? tools : [tools];
for (const { name, description, schema, handler } of toolArray) {
server.tool(name, description, schema, handler);
}
}
/**
* Enhances the McpServer class with a method to register tools using a single object
* This adds the ability to use server.tool(anyTool) syntax
*/
export function enhanceMcpServer(): void {
// Store the original tool method
const originalTool = McpServer.prototype.tool;
// Replace it with our enhanced version that accepts either the original arguments or a tool object
// @ts-expect-error - Intentional prototype modification
McpServer.prototype.tool = function(
nameOrTool: string | ToolDefinition,
description?: string,
schema?: ZodRawShape,
handler?: (args: { [x: string]: unknown }, extra: RequestHandlerExtra) => Promise<CallToolResult>
): void {
// Check if first argument is a ToolDefinition
if (
typeof nameOrTool === "object" &&
nameOrTool !== null &&
"name" in nameOrTool &&
"description" in nameOrTool &&
"schema" in nameOrTool &&
"handler" in nameOrTool
) {
const tool = nameOrTool as ToolDefinition;
return originalTool.call(this, tool.name, tool.description, tool.schema, tool.handler);
}
// Validate arguments for standard call format
if (typeof nameOrTool !== "string") {
throw new TypeError("First argument must be a string when not using object syntax");
}
if (!description) {
throw new Error("Description is required when using standard syntax");
}
if (!schema) {
throw new Error("Schema is required when using standard syntax");
}
if (!handler) {
throw new Error("Handler is required when using standard syntax");
}
return originalTool.call(this, nameOrTool, description, schema, handler);
};
}
/**
* Creates a text response for a tool
* @param text - The text to include in the response
* @returns A properly formatted tool response
*/
export function createTextResponse(text: string): CallToolResult {
return {
content: [{
type: "text",
text
}]
};
}
/**
* Creates an error response
* @param message - The error message
* @param metadata - Optional additional metadata
* @returns A properly formatted error response
*/
export function createErrorResponse(message: string, metadata?: Record<string, unknown>): CallToolResult {
return {
content: [{
type: "text",
text: message
}],
isError: true,
_meta: {
timestamp: new Date().toISOString(),
...metadata
}
};
}