/**
* Shared tool registry for Linear MCP.
* Tools defined here work in both Node.js and Cloudflare Workers.
*/
import type { ZodObject, ZodRawShape } from 'zod';
import {
workspaceMetadataTool,
listIssuesTool,
getIssuesTool,
createIssuesTool,
updateIssuesTool,
listTeamsTool,
listUsersTool,
listCommentsTool,
addCommentsTool,
updateCommentsTool,
listCyclesTool,
listProjectsTool,
createProjectsTool,
updateProjectsTool,
} from './linear/index.js';
import type { ToolContext, ToolResult } from './types.js';
// Re-export types for convenience
export type { SharedToolDefinition, ToolContext, ToolResult } from './types.js';
export { defineTool } from './types.js';
/**
* Simplified tool interface for the registry (type-erased for storage).
*/
export interface RegisteredTool {
name: string;
title?: string;
description: string;
inputSchema: ZodObject<ZodRawShape>;
outputSchema?: ZodRawShape;
annotations?: Record<string, unknown>;
handler: (args: Record<string, unknown>, context: ToolContext) => Promise<ToolResult>;
}
/**
* All shared tools available in both runtimes.
*/
export const sharedTools: RegisteredTool[] = [
// Linear tools - Workspace
workspaceMetadataTool as unknown as RegisteredTool,
// Linear tools - Issues
listIssuesTool as unknown as RegisteredTool,
getIssuesTool as unknown as RegisteredTool,
createIssuesTool as unknown as RegisteredTool,
updateIssuesTool as unknown as RegisteredTool,
// Linear tools - Teams & Users
listTeamsTool as unknown as RegisteredTool,
listUsersTool as unknown as RegisteredTool,
// Linear tools - Comments
listCommentsTool as unknown as RegisteredTool,
addCommentsTool as unknown as RegisteredTool,
updateCommentsTool as unknown as RegisteredTool,
// Linear tools - Cycles
listCyclesTool as unknown as RegisteredTool,
// Linear tools - Projects
listProjectsTool as unknown as RegisteredTool,
createProjectsTool as unknown as RegisteredTool,
updateProjectsTool as unknown as RegisteredTool,
];
/**
* Get a tool by name.
*/
export function getSharedTool(name: string): RegisteredTool | undefined {
return sharedTools.find((t) => t.name === name);
}
/**
* Get all tool names.
*/
export function getSharedToolNames(): string[] {
return sharedTools.map((t) => t.name);
}
/**
* Execute a shared tool by name.
* Handles input validation, output validation, and error wrapping.
*/
export async function executeSharedTool(
name: string,
args: Record<string, unknown>,
context: ToolContext,
): Promise<ToolResult> {
const tool = getSharedTool(name);
if (!tool) {
return {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
isError: true,
};
}
try {
// Check for cancellation before starting
if (context.signal?.aborted) {
return {
content: [{ type: 'text', text: 'Operation was cancelled' }],
isError: true,
};
}
// Validate input using Zod schema
const parseResult = tool.inputSchema.safeParse(args);
if (!parseResult.success) {
const errors = parseResult.error.errors
.map(
(e: { path: (string | number)[]; message: string }) =>
`${e.path.join('.')}: ${e.message}`,
)
.join(', ');
return {
content: [{ type: 'text', text: `Invalid input: ${errors}` }],
isError: true,
};
}
const result = await tool.handler(
parseResult.data as Record<string, unknown>,
context,
);
// Validate outputSchema compliance (per MCP spec)
if (tool.outputSchema && !result.isError) {
if (!result.structuredContent) {
return {
content: [
{
type: 'text',
text: 'Tool with outputSchema must return structuredContent (unless isError is true)',
},
],
isError: true,
};
}
}
return result;
} catch (error) {
// Check if this was an abort
if (context.signal?.aborted) {
return {
content: [{ type: 'text', text: 'Operation was cancelled' }],
isError: true,
};
}
return {
content: [{ type: 'text', text: `Tool error: ${(error as Error).message}` }],
isError: true,
};
}
}