Skip to content

feat: add support for Linear documents API #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
261 changes: 260 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { LinearAPIService } from './services/linear/index';
import { isCreateCommentArgs, isCreateIssueArgs, isCreateProjectUpdateArgs, isDeleteIssueArgs, isGetIssueArgs, isGetProjectUpdatesArgs, isGetProjectsArgs, isGetTeamsArgs, isSearchIssuesArgs, isUpdateIssueArgs } from './types/linear/index';
import {
isCreateCommentArgs, isCreateDocumentArgs, isCreateIssueArgs, isCreateProjectUpdateArgs,
isDeleteDocumentArgs, isDeleteIssueArgs, isGetDocumentArgs, isGetDocumentsArgs, isGetIssueArgs,
isGetProjectUpdatesArgs, isGetProjectsArgs, isGetTeamsArgs, isSearchIssuesArgs,
isUpdateDocumentArgs, isUpdateIssueArgs
} from './types/linear/index';

// Get Linear API key from environment variable
const API_KEY = process.env.LINEAR_API_KEY;
Expand Down Expand Up @@ -332,6 +337,155 @@ class LinearServer {
required: ['projectId']
}
},
{
name: 'get_documents',
description: 'Get a list of Linear documents with optional name filtering and pagination\n\nExamples:\n1. Basic usage: {}\n2. Filter by name: {nameFilter: \"meeting notes\"}\n3. Include archived: {includeArchived: true, nameFilter: \"roadmap\"}\n4. Pagination: {first: 25, after: \"cursor-from-previous-response\"}\n5. Filter by project: {projectId: \"project-123\"}\n6. Filter by team: {teamId: \"team-abc\"}\n\nReturns a paginated list of documents with metadata including:\n- Document details (id, title, content preview, etc.)\n- Creator information\n- Last editor information \n- Project and team associations\n- Creation and update timestamps',
inputSchema: {
type: 'object',
properties: {
nameFilter: {
type: 'string',
description: 'Optional filter to search by document title'
},
includeArchived: {
type: 'boolean',
description: 'Whether to include archived documents (default: true)',
default: true
},
teamId: {
type: 'string',
description: 'Filter documents by team ID'
},
projectId: {
type: 'string',
description: 'Filter documents by project ID'
},
first: {
type: 'number',
description: 'Number of items to return (default: 50, max: 100)',
default: 50
},
after: {
type: 'string',
description: 'Cursor for pagination. Use the endCursor from a previous response to fetch the next page'
}
}
}
},
{
name: 'get_document',
description: 'Get detailed information about a specific Linear document\n\nReturns comprehensive document information including:\n- Full document content in markdown format\n- Metadata (creation date, last edited date, etc.)\n- Creator and editor information\n- Team and project associations\n- Document URL\n\nYou can retrieve a document using either its ID or its URL slug.',
inputSchema: {
type: 'object',
properties: {
documentId: {
type: 'string',
description: 'The ID of the Linear document'
},
documentSlug: {
type: 'string',
description: 'The URL slug of the document (alternative to documentId)'
},
includeFull: {
type: 'boolean',
description: 'Whether to include the full document content (default: true)',
default: true
}
},
required: ['documentId']
}
},
{
name: 'create_document',
description: 'Create a new document in Linear\n\nExamples:\n1. Basic document: {teamId: \"team-123\", title: \"Meeting Notes\", content: \"# Meeting Notes\\n\\nDiscussion points...\"}\n2. Project document: {teamId: \"team-123\", projectId: \"project-abc\", title: \"Design Spec\"}\n3. With icon: {teamId: \"team-123\", title: \"Design System\", icon: \"🎨\", content: \"...\"}\n\nReturns the newly created document with its ID, URL, and other metadata.',
inputSchema: {
type: 'object',
properties: {
teamId: {
type: 'string',
description: 'ID of the team this document belongs to'
},
title: {
type: 'string',
description: 'Title of the document'
},
content: {
type: 'string',
description: 'Content of the document in markdown format'
},
icon: {
type: 'string',
description: 'Emoji icon for the document'
},
projectId: {
type: 'string',
description: 'ID of the project to associate this document with'
},
isPublic: {
type: 'boolean',
description: 'Whether the document should be accessible outside the organization (default: false)',
default: false
}
},
required: ['teamId', 'title']
}
},
{
name: 'update_document',
description: 'Update an existing Linear document\n\nExamples:\n1. Update content: {documentId: \"doc-123\", content: \"# Updated Content\"}\n2. Update title: {documentId: \"doc-123\", title: \"New Title\"}\n3. Change icon: {documentId: \"doc-123\", icon: \"🚀\"}\n4. Change project: {documentId: \"doc-123\", projectId: \"project-456\"}\n5. Archive: {documentId: \"doc-123\", isArchived: true}\n\nYou only need to include the fields you want to update. Returns the updated document with its new values.',
inputSchema: {
type: 'object',
properties: {
documentId: {
type: 'string',
description: 'ID of the document to update'
},
title: {
type: 'string',
description: 'New title for the document'
},
content: {
type: 'string',
description: 'New content for the document in markdown format'
},
icon: {
type: 'string',
description: 'New emoji icon for the document'
},
projectId: {
type: 'string',
description: 'ID of the project to move this document to'
},
teamId: {
type: 'string',
description: 'ID of the team to move this document to'
},
isArchived: {
type: 'boolean',
description: 'Whether the document should be archived'
},
isPublic: {
type: 'boolean',
description: 'Whether the document should be accessible outside the organization'
}
},
required: ['documentId']
}
},
{
name: 'delete_document',
description: 'Delete an existing Linear document\n\nThis action permanently removes the document from Linear and cannot be undone. Returns a success message when the document is successfully deleted.',
inputSchema: {
type: 'object',
properties: {
documentId: {
type: 'string',
description: 'ID of the document to delete'
}
},
required: ['documentId']
}
},
],
}));

Expand All @@ -358,6 +512,16 @@ class LinearServer {
return await this.handleGetProjectUpdates(request.params.arguments);
case 'create_project_update':
return await this.handleCreateProjectUpdate(request.params.arguments);
case 'get_documents':
return await this.handleGetDocuments(request.params.arguments);
case 'get_document':
return await this.handleGetDocument(request.params.arguments);
case 'create_document':
return await this.handleCreateDocument(request.params.arguments);
case 'update_document':
return await this.handleUpdateDocument(request.params.arguments);
case 'delete_document':
return await this.handleDeleteDocument(request.params.arguments);
default:
throw new McpError(
ErrorCode.MethodNotFound,
Expand Down Expand Up @@ -557,6 +721,101 @@ class LinearServer {
};
}

private async handleGetDocuments(args: unknown) {
if (!isGetDocumentsArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid get_documents arguments'
);
}

const documents = await this.linearAPI.getDocuments(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(documents, null, 2)
}
]
};
}

private async handleGetDocument(args: unknown) {
if (!isGetDocumentArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid get_document arguments'
);
}

const document = await this.linearAPI.getDocument(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(document, null, 2)
}
]
};
}

private async handleCreateDocument(args: unknown) {
if (!isCreateDocumentArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid create_document arguments'
);
}

const document = await this.linearAPI.createDocument(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(document, null, 2)
}
]
};
}

private async handleUpdateDocument(args: unknown) {
if (!isUpdateDocumentArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid update_document arguments'
);
}

const document = await this.linearAPI.updateDocument(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(document, null, 2)
}
]
};
}

private async handleDeleteDocument(args: unknown) {
if (!isDeleteDocumentArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid delete_document arguments'
);
}

const result = await this.linearAPI.deleteDocument(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}

async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
Expand Down
Loading