Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ npm install
npm run dev -- --token YOUR_LINEAR_API_TOKEN
```

To exercise the alpha managed OAuth application tools, use an eligible managing OAuth application's access token instead:

```bash
npm run dev -- --oauth-token YOUR_LINEAR_OAUTH_ACCESS_TOKEN
```

Do not use production OAuth applications for destructive live tests. Use temporary child-app and webhook names, capture one-time secrets without logging them, and archive/delete every created resource before finishing.

### Validation

Use the following checks before merging or publishing:
Expand Down
44 changes: 40 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ MCP Linear bridges AI assistants and Linear by implementing the MCP protocol. Wi
- Retrieve issues, projects, teams, cycles, milestones, roadmaps, customers, customer needs, and workspace/project/initiative/team/issue/release/cycle documents
- Create and update issues, change status, assign, and comment
- Manage projects, full diff-aware project and initiative update lifecycles, milestones, roadmaps, saved views, and favorites
- Work with templates, custom fields, webhooks, and attachments
- Create and manage workspace webhooks, including updates and signing-secret rotation
- Prepare OAuth app manifests and authorization URLs, issue scoped client-credentials tokens, or manage child OAuth apps when authenticated as a managing OAuth application
- Work with templates, custom fields, and attachments
- Work with customer records, customer statuses/tiers, and customer needs linked to issues or projects
- Read notifications, subscriptions, sessions, audits, and integrations without leaving MCP
- Inspect rate-limit and server health before running heavy planning sessions
Expand Down Expand Up @@ -50,10 +52,15 @@ Once connected, you can use prompts like:
- "Get the latest project update diff and archive an outdated update"
- "Show customer needs for this project and mark the important ones"
- "Create an initiative update and hide the generated diff from the update body"
- "Prepare a private OAuth app for my GitHub issue pipeline with client credentials enabled"
- "Issue a narrowly scoped client-credentials token for that GitHub pipeline"
- "Create a webhook for Issue and Comment events, then rotate its signing secret"

## Installation

### Getting your Linear API token
### Authentication

#### Personal API key (default)

1. Log in to your Linear account at [linear.app](https://linear.app)
2. Click on your organization avatar (top-left corner)
Expand All @@ -63,6 +70,29 @@ Once connected, you can use prompts like:
6. Give your key a name (e.g., `MCP Linear Integration`)
7. Copy the generated API token and store it securely — you won't be able to see it again

Personal API keys support the normal Linear and workspace-webhook tools. They cannot call Linear's alpha managed-child-OAuth-application API because that API requires the caller itself to be an OAuth application. With a personal API key, `linear_generateOAuthApplicationSetup` still prepares an official manifest and pre-filled Linear setup URL for an administrator to confirm.

#### OAuth access token (managed OAuth applications)

To let MCP actually create and manage child OAuth applications, authenticate it with an access token belonging to a Linear OAuth application that is eligible to manage those child applications:

```bash
export LINEAR_OAUTH_ACCESS_TOKEN=YOUR_OAUTH_ACCESS_TOKEN
mcp-linear
```

Or pass `--oauth-token YOUR_OAUTH_ACCESS_TOKEN`. Explicit command-line credentials take precedence over environment variables; when both environment credential types are present, OAuth authentication is selected. See Linear's [OAuth documentation](https://linear.app/developers/oauth-2-0-authentication) and [OAuth application manifests](https://linear.app/developers/oauth-app-manifests).

Each MCP server process uses one Linear credential. If the managing app token uses `actor=app` (which cannot receive `admin`) and you also need admin-scoped workspace-webhook tools, configure two MCP server entries: one with the managing OAuth token for child-app operations and one with a workspace administrator's personal API key for workspace webhooks. A user-actor OAuth token carrying `admin` can cover the webhook side instead.

OAuth scopes are selected when an authorization URL or client-credentials token is requested; they are not mutable fields on an OAuth application. The MCP validates current Linear scopes, prepares authorization URLs, and can issue app-actor tokens with `linear_createOAuthClientCredentialsToken`. For GitHub-hosted pipelines, enable the `client_credentials` grant and request the narrowest useful scope, such as `issues:create`.

Client-credentials tokens normally expire after 30 days and have no refresh token. Linear permits multiple active tokens only while they use the same scope set; requesting a different scope set revokes the application's existing app-actor tokens. The token tool therefore requires both `confirmSecretExposure: true` and `confirmScopeChangeRisk: true`.

Creating an OAuth app and rotating OAuth or webhook secrets returns one-time secret material through MCP. Those tools require `confirmSecretExposure: true`; move returned values directly into a secret manager such as GitHub Actions secrets and do not paste them into source control or logs.

Webhook URLs must be publicly reachable HTTPS endpoints. Validation rejects credentials in URLs and obvious loopback, private-network, link-local, and local-hostname destinations.

### Installing via [add-mcp](https://github.com/neondatabase/add-mcp) (Recommended)

`add-mcp` installs the server into Claude Code, Cursor, Codex, VS Code, Claude Desktop, and many other MCP-aware agents with a single command:
Expand Down Expand Up @@ -102,9 +132,9 @@ Add the following to your MCP settings file:

Prerequisites:

- Node.js (v18+)
- Node.js (v20+)
- NPM or Yarn
- Linear API token
- Linear personal API key or OAuth access token

```bash
# Install globally
Expand All @@ -125,6 +155,12 @@ Run the server with your Linear API token:
mcp-linear --token YOUR_LINEAR_API_TOKEN
```

Or with a managing OAuth application's access token:

```bash
mcp-linear --oauth-token YOUR_OAUTH_ACCESS_TOKEN
```

Or set the token in your environment and run without arguments:

```bash
Expand Down
54 changes: 45 additions & 9 deletions TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,13 +316,49 @@ Linear calls these "saved views" in the product UI. The GraphQL API and SDK expo

### Webhook and Attachment Tools

| Tool Name | Description | Status |
| ----------------------- | --------------------------------------- | -------------- |
| `linear_getWebhooks` | Get a list of webhooks | ✅ Implemented |
| `linear_createWebhook` | Create a webhook for integration events | ✅ Implemented |
| `linear_deleteWebhook` | Delete a webhook | ✅ Implemented |
| `linear_getAttachments` | Get attachments for an issue | ✅ Implemented |
| `linear_addAttachment` | Add an attachment to an issue | ✅ Implemented |
| Tool Name | Description | Status |
| --------------------------- | ------------------------------------------------ | -------------- |
| `linear_getWebhooks` | Get a list of workspace webhooks | ✅ Implemented |
| `linear_getWebhookById` | Get one workspace webhook | ✅ Implemented |
| `linear_createWebhook` | Create a workspace webhook | ✅ Implemented |
| `linear_updateWebhook` | Update a workspace webhook | ✅ Implemented |
| `linear_rotateWebhookSecret` | Rotate and return a new webhook signing secret | ✅ Implemented |
| `linear_deleteWebhook` | Delete a workspace webhook | ✅ Implemented |
| `linear_getAttachments` | Get attachments for an issue | ✅ Implemented |
| `linear_addAttachment` | Add an attachment to an issue | ✅ Implemented |

Workspace webhooks are separate from the installation webhook configured on an OAuth application. Workspace webhook reads and mutations require a workspace administrator or an OAuth token with Linear's `admin` scope. Secret rotation returns the replacement secret once and invalidates the previous signing secret immediately; callers that let Linear generate a secret during creation can rotate it before enabling deliveries to capture a known value securely.

Linear types ordinary workspace-webhook `resourceTypes` as a list of strings, so those tools accept unique non-empty resource names without freezing the surface to today's OAuth installation-webhook enum. OAuth application webhooks remain validated against Linear's current 22-value manifest/GraphQL enum.

Webhook destinations must be publicly reachable HTTPS URLs. Runtime validation rejects embedded URL credentials and obvious loopback, private-network, link-local, and local-hostname destinations.

### OAuth Application Tools

| Tool Name | Description | Status |
| ------------------------------------------------------- | ---------------------------------------------------------------------------- | -------------- |
| `linear_generateOAuthApplicationSetup` | Generate an official manifest and pre-filled setup URL; a human confirms it | ✅ Implemented |
| `linear_generateOAuthAuthorizationUrl` | Generate a validated OAuth authorization URL and scope request | ✅ Implemented |
| `linear_createOAuthClientCredentialsToken` | Issue a scoped app-actor token for server-to-server pipelines | ✅ Implemented |
| `linear_getManagedOAuthApplications` | List child OAuth apps owned by the calling OAuth application | ✅ Implemented |
| `linear_getManagedOAuthApplicationById` | Get one child OAuth app owned by the calling OAuth application | ✅ Implemented |
| `linear_createManagedOAuthApplication` | Create a child OAuth app and return its one-time secrets | ✅ Implemented |
| `linear_updateManagedOAuthApplication` | Update a child OAuth app | ✅ Implemented |
| `linear_archiveManagedOAuthApplication` | Archive a child OAuth app | ✅ Implemented |
| `linear_rotateManagedOAuthApplicationSecret` | Rotate and return a child app's client secret | ✅ Implemented |
| `linear_rotateManagedOAuthApplicationWebhookSecret` | Rotate and return a child app's installation-webhook secret | ✅ Implemented |

Linear exposes managed OAuth application lifecycle operations as an alpha GraphQL surface and does not yet include first-class methods for them in `@linear/sdk`; this server therefore uses minimal raw GraphQL for that slice. The operations only manage applications created by the calling OAuth application. Configure `LINEAR_OAUTH_ACCESS_TOKEN` or `--oauth-token` with an eligible managing application's access token; a personal API key has no calling OAuth application identity and cannot use these operations.

One server process uses one Linear credential. When the managing app uses `actor=app` (which cannot receive `admin`), use separate MCP server entries for managed-child-app operations and admin-scoped workspace webhooks, or use an eligible user-actor OAuth token with `admin` for the webhook entry.

For the common API-key workflow, `linear_generateOAuthApplicationSetup` does not claim to create anything. It returns a manifest plus Linear's official pre-filled creation URL, where a workspace administrator reviews and confirms the app. The manifest supports `authorization_code` and `client_credentials` grants and OAuth installation webhooks.

Scopes are grants requested through `/oauth/authorize` or `/oauth/token`, not fields stored on an OAuth application. `linear_generateOAuthAuthorizationUrl` validates Linear's current user and app-actor scopes and rejects the unsupported `actor=app` plus `admin` combination. `linear_createOAuthClientCredentialsToken` completes the non-interactive pipeline flow by issuing an app-actor token directly from a client ID and secret.

Client-credentials tokens normally last 30 days and do not include refresh tokens. Linear allows multiple active tokens only when they share the same scope set; requesting a different scope set revokes existing app-actor tokens. Token issuance therefore requires explicit acknowledgement of both secret exposure and scope-change risk. See Linear's [OAuth guide](https://linear.app/developers/oauth-2-0-authentication), [agent scopes](https://linear.app/developers/agents), and [manifest format](https://linear.app/developers/oauth-app-manifests).

All operations that return a newly generated secret require `confirmSecretExposure: true`. Client secrets and signing secrets are one-time values and should be transferred directly to a secret manager.

### Notification and Session Tools

Expand Down Expand Up @@ -437,7 +473,7 @@ Bulk import/export is feasible, but it does not map cleanly to first-class SDK b

These are directly supported by the current SDK and fit the repo well.

Webhooks, attachment reads, and attachment creation are now covered.
The workspace webhook lifecycle, attachment reads, and attachment creation are now covered.

### Integrations

Expand Down Expand Up @@ -502,7 +538,7 @@ Rich reporting support appears thin in the current SDK beyond export/report help

### Sessions and Authentication

Session and audit reads are a better fit for this repo than broader API-key or OAuth-client admin flows.
Session management and managed OAuth application workflows are covered. Personal API-key creation remains a Linear settings operation and is intentionally not exposed as an MCP tool.

| Tool Name | Description | Priority | Status |
| -------------------------------- | ---------------------------------------- | -------- | ---------- |
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tacticlaunch/mcp-linear",
"version": "1.3.1",
"version": "1.4.0",
"description": "A Model Context Protocol (MCP) server implementation for the Linear GraphQL API that enables AI assistants to interact with Linear project management systems.",
"main": "dist/index.js",
"type": "module",
Expand Down Expand Up @@ -43,7 +43,7 @@
"url": "git+https://github.com/tacticlaunch/mcp-linear.git"
},
"dependencies": {
"@linear/sdk": "^86.0.0",
"@linear/sdk": "^88.1.0",
"@modelcontextprotocol/sdk": "^1.6.0",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
Expand Down
48 changes: 48 additions & 0 deletions scripts/mcp-smoke-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ const criticalToolNames = [
'linear_updateMilestone',
'linear_updateSavedView',
'linear_removeFromFavorites',
'linear_generateOAuthApplicationSetup',
'linear_generateOAuthAuthorizationUrl',
'linear_createOAuthClientCredentialsToken',
'linear_createManagedOAuthApplication',
'linear_getWebhookById',
'linear_updateWebhook',
'linear_rotateWebhookSecret',
];

const scriptPath = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -46,6 +53,11 @@ async function main() {
expectedToolNames,
`MCP server advertised an unexpected tool set. Expected ${expectedToolNames.length} tools, got ${actualToolNames.length}.`,
);
assert.equal(
new Set(actualToolNames).size,
actualToolNames.length,
'MCP server advertised duplicate tool names.',
);

for (const tool of tools) {
assert.equal(
Expand All @@ -66,6 +78,42 @@ async function main() {
assert.ok(actualToolNames.includes(toolName), `Expected tool ${toolName} to be registered.`);
}

const setupResult = await client.callTool({
name: 'linear_generateOAuthApplicationSetup',
arguments: {
name: 'MCP smoke agent',
developer: 'Tactic Launch',
developerUrl: 'https://example.com/linear',
redirectUris: ['https://example.com/oauth/callback'],
grantTypes: ['authorization_code', 'client_credentials'],
},
});
assert.equal(setupResult.isError, false, 'OAuth application setup generator should run without Linear I/O.');
const setupPayload = JSON.parse(setupResult.content[0].text);
assert.equal(setupPayload.requiresUserConfirmation, true);
assert.ok(setupPayload.creationUrl.startsWith('https://linear.app/settings/api/applications/new?'));

const authorizationResult = await client.callTool({
name: 'linear_generateOAuthAuthorizationUrl',
arguments: {
clientId: 'mcp-smoke-client',
redirectUri: 'https://example.com/oauth/callback',
scopes: ['issues:create'],
actor: 'app',
state: 'mcp-smoke-state',
},
});
assert.equal(authorizationResult.isError, false, 'OAuth authorization URL generator should run without Linear I/O.');
const authorizationPayload = JSON.parse(authorizationResult.content[0].text);
assert.deepEqual(authorizationPayload.scopes, ['read', 'issues:create']);

const rejectedSecretRotation = await client.callTool({
name: 'linear_rotateWebhookSecret',
arguments: { id: 'webhook-1', confirmSecretExposure: false },
});
assert.equal(rejectedSecretRotation.isError, true, 'Secret rotation must require explicit exposure acknowledgement.');
assert.ok(rejectedSecretRotation.content[0].text.includes('Invalid arguments for rotateWebhookSecret'));

const { resources } = await client.listResources();
const resourceUris = resources.map((resource) => resource.uri);
assert.ok(resourceUris.includes('linear://viewer'), 'Expected linear://viewer resource to be registered.');
Expand Down
Loading