/**
* Centralized tool metadata for the Linear MCP server.
*/
export interface ToolMetadata {
name: string;
title: string;
description: string;
}
export const serverMetadata = {
title: 'Linear',
instructions: `Use these tools to list and manage Linear issues, projects, teams, and users.
Quick start
- Call 'workspace_metadata' first to fetch canonical identifiers you will reuse across tools. It will include your viewer id: use that as 'assigneeId' when you want to assign items to yourself.
- Then use 'list_issues' with teamId/projectId and filters (or q/keywords) to locate targets. Use 'list_issues' for both discovery and precise lookups (including by id/identifier) — prefer orderBy='updatedAt'.
- To modify, use 'update_issues' / 'update_projects', then verify with 'list_issues'.
- For teams with cycles enabled, use 'list_cycles' to browse planning cycles.
Default recency window
- If a date range is not provided when listing issues, default to the current week in the viewer's timezone (Mon 00:00 → Sun 23:59:59.999), using updatedAt for recency. Mention the timezone surfaced by the client in your reasoning and outputs.
Account identifiers (returned by 'workspace_metadata')
- viewer: { id, name, email, displayName, avatarUrl, timezone, createdAt }
- teams: Array<{ id, key, name, description?, defaultIssueEstimate? }>
- workflowStatesByTeam: Record<teamId, Array<{ id, name, type }>>
- labelsByTeam: Record<teamId, Array<{ id, name, color?, description? }>>
- projects: Array<{ id, name, state, teamId?, leadId?, targetDate?, createdAt }>
How to chain safely
- teamId: filter in 'list_issues'; pass to 'create_issues'; use workflowStatesByTeam[teamId] to find stateId for 'update_issues'.
- projectId: filter in 'list_issues'; pass to create/update issue payloads.
- stateId: set via 'update_issues'. Resolve by name → id using workflowStatesByTeam.
- labelIds: pass to create/update; resolve from labelsByTeam.
Handling assignees and failures
- To assign to yourself, prefer using your viewer id from 'workspace_metadata' as 'assigneeId'.
- If a create or update fails with 'assigneeId ... could not be found', either:
- Re-run 'workspace_metadata' and verify the correct team/project and user id, or
- Use 'list_users' to fetch users and pick the right id.
Filtering (list_issues)
- 'filter' supports GraphQL-style comparators and relationship fields. Comparators: { eq, neq, lt, lte, gt, gte, in, nin, containsIgnoreCase, startsWith, endsWith, null }. Common examples:
- Team/project: { team: { id: { eq: teamId } } } or { project: { id: { eq: projectId } } }
- State type: { state: { type: { eq: "started" } } }
- Assignee email: { assignee: { email: { eqIgnoreCase: "name@acme.com" } } }
- Title case-insensitive contains: { title: { containsIgnoreCase: "search" } }
- Labels (use names from your workspace): { labels: { name: { in: ["LabelA", "LabelB"] } } }
- Or use q/keywords for keyword search (default matchMode='all' requires ALL tokens; use 'any' for broad search).
- Use workspace_metadata first to discover team/project/label/user names and IDs.
Pagination
- List tools return { cursor, nextCursor, limit }. Pass nextCursor to fetch the next page.
- Prefer small limits and refine filters instead of broad scans.
Safety & writes
- Do not guess ids. Always take ids from 'workspace_metadata' or a read tool.
- Batch writes default to sequential; keep batches small and verify with 'list_issues'.
- 'update_issues' ignores empty strings (e.g., dueDate: ""): only valid fields are sent.
`,
} as const;
export const toolsMetadata = {
workspace_metadata: {
name: 'workspace_metadata',
title: 'Discover IDs (Use First)',
description:
"Use this to discover workspace entities and canonical IDs (viewer, teams, workflow states, labels, projects, favorites). Use this FIRST whenever you don't know ids. Inputs: include? (profile|teams|workflow_states|labels|projects|favorites), teamIds?, project_limit?, label_limit?.\nReturns: viewer, teams[] (with estimation settings and cyclesEnabled), workflowStatesByTeam, labelsByTeam, projects[], favorites?. Next: Use teamId/projectId to filter 'list_issues'; use workflowStatesByTeam[teamId][].id as stateId for 'update_issues'; use labelsByTeam ids for label operations. If a team has cyclesEnabled=false, avoid cycle-related tools.",
},
list_issues: {
name: 'list_issues',
title: 'List Issues',
description:
'List issues with filtering. Inputs: teamId?, projectId?, filter?, q?, keywords?, matchMode?, includeArchived?, orderBy?(updatedAt|createdAt), detail?(minimal|standard|full), limit?, cursor?, assignedToMe?.\n\n⚠️ orderBy only supports updatedAt or createdAt. DO NOT use orderBy:"priority" - use filter instead!\n\nKEYWORD SEARCH (q/keywords):\n- q: Extract 2-4 significant keywords from user intent. Avoid short/common words.\n- matchMode: \'all\' (default, precise) requires ALL tokens; \'any\' (broad) requires at least ONE.\n- Example: user says "find cursor workshop task" → q: "cursor workshop"\n\nFILTERING:\n- High priority: filter: { priority: { lte: 2 } } (1=Urgent, 2=High, 3=Medium, 4=Low)\n- Active issues: filter: { state: { type: { neq: \'completed\' } } }\n- In progress: filter: { state: { type: { eq: \'started\' } } }\n- My issues: assignedToMe: true\n\nDETAIL LEVELS: minimal (id,title,state), standard (default, +priority,assignee,project), full (+labels,description).\n\nReturns: { items[], pagination, meta }.',
},
get_issues: {
name: 'get_issues',
title: 'Get Issues (Batch)',
description:
"Fetch detailed issues in batch by ids (UUIDs or short ids like ENG-123). Inputs: { ids: string[] }.\nReturns: { results: Array<{ index, ok, id?, identifier?, issue? }>, summary }. Each issue includes assignee, state, project, labels, attachments, and branchName when available. Next: Call 'update_issues' to modify fields or 'list_issues' to discover more.",
},
create_issues: {
name: 'create_issues',
title: 'Create Issues (Batch)',
description:
"Create multiple issues in one call. Inputs: { items: Array<{ teamId: string; title: string; description?; stateId?; stateName?; stateType?; labelIds?; labelNames?; assigneeId?; assigneeName?; assigneeEmail?; projectId?; projectName?; priority?; estimate?; dueDate?; parentId?; allowZeroEstimate? }>; parallel?; dry_run? }.\n\nHUMAN-READABLE INPUTS (use workspace_metadata to discover valid values):\n- priority: 0-4 or \"Urgent\"/\"High\"/\"Medium\"/\"Low\" (standardized)\n- stateType: \"completed\"/\"started\"/\"backlog\"/\"unstarted\"/\"canceled\" (standardized)\n- stateName/labelNames/assigneeName/projectName: workspace-specific\n\nBehavior: Only send fields you intend to set. If 'assigneeId' is omitted, defaults to current viewer. Invalid numbers are ignored (priority<0 dropped; estimate<=0 dropped unless allowZeroEstimate=true). Returns: per-item results with id/identifier. Next: verify with 'list_issues'.",
},
update_issues: {
name: 'update_issues',
title: 'Update Issues (Batch)',
description:
"Update issues in batch (state, labels, assignee, metadata). Supports up to 50 items per call — always batch all updates together. Inputs: { items: Array<{ id: string; title?; description?; stateId?; stateName?; stateType?; labelIds?; labelNames?; addLabelIds?; addLabelNames?; removeLabelIds?; removeLabelNames?; assigneeId?; assigneeName?; assigneeEmail?; projectId?; projectName?; priority?; estimate?; dueDate?; parentId?; archived?; allowZeroEstimate? }>; parallel?; dry_run? }.\n\nHUMAN-READABLE INPUTS (use workspace_metadata to discover valid values):\n- priority: 0-4 or \"Urgent\"/\"High\"/\"Medium\"/\"Low\" (standardized)\n- stateType: \"completed\"/\"started\"/\"canceled\" (standardized)\n- stateName/labelNames/assigneeName/projectName: workspace-specific\n\nBehavior: Only send fields you intend to change. Empty strings ignored; estimate<=0 ignored unless allowZeroEstimate=true. add/removeLabelIds/Names adjust labels incrementally.\nExample: { items: [{ id: 'ABC-123', stateType: 'completed', priority: 'High' }] }. Returns: per-item results. Next: 'get_issues' for verification.",
},
list_projects: {
name: 'list_projects',
title: 'List Projects',
description:
"List projects with filtering and pagination. Inputs: filter? (ProjectFilter: id/state/team/lead/targetDate), includeArchived?, limit?, cursor?. For a single project, set filter.id.eq and limit=1.\nReturns: { items[], cursor?, nextCursor?, limit? } where items include id, name, state, leadId?, teamId?, targetDate?, description?. Next: Use 'update_projects' to modify or 'list_issues' with projectId to find issues.",
},
create_projects: {
name: 'create_projects',
title: 'Create Projects (Batch)',
description:
'Create multiple projects in one call. Inputs: { items: Array<{ name: string; teamId?: string; leadId?: string; description?: string; targetDate?: string; state?: string }> }.\nNotes: team association uses teamIds internally; provide teamId to attach initially. Returns: per-item results and a summary.',
},
update_projects: {
name: 'update_projects',
title: 'Update Projects (Batch)',
description:
"Update multiple projects in one call. Inputs: { items: Array<{ id: string; name?: string; description?: string; targetDate?: string; state?: string; leadId?: string; archived?: boolean }> }.\nReturns: per-item results and a summary. Next: verify with 'list_projects' (filter.id.eq, limit=1); discover via 'list_projects'.",
},
list_teams: {
name: 'list_teams',
title: 'List Teams',
description:
"List teams in the workspace. Inputs: limit?, cursor?.\nReturns: { items: Array<{ id, key?, name }>, cursor?, nextCursor?, limit? }. Next: Use team ids with 'workspace_metadata' (workflowStatesByTeam) and 'list_issues'.",
},
list_users: {
name: 'list_users',
title: 'List Users',
description:
"List users in the workspace. Inputs: limit?, cursor?.\nReturns: { items: Array<{ id, name?, email?, displayName?, avatarUrl? }>, cursor?, nextCursor?, limit? }. Next: Use user ids in 'update_issues' (assigneeId).",
},
list_comments: {
name: 'list_comments',
title: 'List Comments',
description:
'List comments for an issue. Inputs: { issueId, limit?, cursor? }.\nReturns: { items[], cursor?, nextCursor?, limit? } where items include id, body, url?, createdAt, updatedAt?, user{id,name?}. Next: Use add_comments to add context or mention teammates.',
},
add_comments: {
name: 'add_comments',
title: 'Add Comments (Batch)',
description:
'Add one or more comments to issues. Inputs: { items: Array<{ issueId: string; body: string }>, parallel?, dry_run? }.\nReturns: per-item results and a summary. Next: Use list_comments to verify and retrieve the comment URLs.',
},
update_comments: {
name: 'update_comments',
title: 'Update Comments (Batch)',
description:
'Update existing comment bodies. Cannot delete comments (by design). Inputs: { items: Array<{ id: string; body: string }> }.\nReturns: per-item results and a summary. Next: Use list_comments to verify changes.',
},
list_cycles: {
name: 'list_cycles',
title: 'List Cycles',
description:
"List cycles for a team (only if team.cyclesEnabled=true). Inputs: { teamId, includeArchived?, orderBy?(updatedAt|createdAt), limit?, cursor? }.\nReturns: { items[], cursor?, nextCursor?, limit? } where items include id, name?, number?, startsAt?, endsAt?, completedAt?, teamId, status?. Next: Use teamId from 'workspace_metadata' to target the right team; avoid this tool if cyclesEnabled=false.",
},
} as const satisfies Record<string, ToolMetadata>;
/**
* Type-safe helper to get metadata for a tool.
*/
export function getToolMetadata(toolName: keyof typeof toolsMetadata): ToolMetadata {
return toolsMetadata[toolName];
}
/**
* Get all registered tool names.
*/
export function getToolNames(): string[] {
return Object.keys(toolsMetadata);
}