Skip to main content
Glama
iceener

Linear Streamable MCP Server

by iceener
client.ts5.93 kB
/** * Linear SDK client factory. * Provides authenticated Linear API clients using the template's auth context. */ import { LinearClient } from '@linear/sdk'; import { config } from '../../config/env.js'; import { getTokenStore } from '../../shared/storage/singleton.js'; import type { ToolContext } from '../../shared/tools/types.js'; import { sharedLogger as logger } from '../../shared/utils/logger.js'; // Cache clients per token to avoid recreating on every call const clientCache = new Map<string, LinearClient>(); /** * Generate cache key from token. * Uses full token - it's already in memory and Map is internal. */ function tokenToCacheKey(prefix: string, token: string): string { return `${prefix}:${token}`; } /** * Helper to create client with correct auth method. * Detects if token is an API key (starts with lin_) or OAuth token. */ function createClient(token: string): LinearClient { return token.startsWith('lin_') ? new LinearClient({ apiKey: token }) : new LinearClient({ accessToken: token }); } /** * Get a Linear API client for the authenticated user. * Uses the provider token from the tool context or falls back to env vars. */ export async function getLinearClient(context?: ToolContext): Promise<LinearClient> { // 1. Try provider token from context (OAuth flow) const providerToken = context?.providerToken || context?.provider?.accessToken; if (providerToken) { const cacheKey = tokenToCacheKey('provider', providerToken); const cached = clientCache.get(cacheKey); if (cached) { return cached; } const client = createClient(providerToken); clientCache.set(cacheKey, client); logger.debug('linear_client', { message: 'Created client from provider token', sessionId: context?.sessionId, }); return client; } // 2. Try RS token mapping (if auth headers present) const authHeader = context?.authHeaders?.authorization; if (authHeader) { const bearerMatch = authHeader.match(/^\s*Bearer\s+(.+)$/i); const rsToken = bearerMatch?.[1]; if (rsToken) { try { const store = getTokenStore(); const record = await store.getByRsAccess(rsToken); if (record?.provider?.access_token) { const cacheKey = tokenToCacheKey('rs', rsToken); const cached = clientCache.get(cacheKey); if (cached) { return cached; } const client = createClient(record.provider.access_token); clientCache.set(cacheKey, client); logger.debug('linear_client', { message: 'Created client from RS token mapping', sessionId: context?.sessionId, }); return client; } } catch (error) { logger.warning('linear_client', { message: 'Failed to look up RS token', error: (error as Error).message, }); } // Assume bearer is a Linear token directly (API key mode) const cacheKey = tokenToCacheKey('bearer', rsToken); const cached = clientCache.get(cacheKey); if (cached) { return cached; } const client = createClient(rsToken); clientCache.set(cacheKey, client); logger.debug('linear_client', { message: 'Created client from direct bearer token', sessionId: context?.sessionId, }); return client; } } // 3. Fall back to environment variable (local dev only) const envAccessToken = config.LINEAR_ACCESS_TOKEN; if (!envAccessToken) { throw new Error( 'Linear OAuth required: complete the OAuth flow to get an access token', ); } const cacheKey = tokenToCacheKey('env', envAccessToken); const cached = clientCache.get(cacheKey); if (cached) { return cached; } const client = createClient(envAccessToken); clientCache.set(cacheKey, client); logger.debug('linear_client', { message: 'Created client from environment variable (local dev)', }); return client; } /** * Synchronous version that throws if async lookup would be needed. * Use this only when you're certain context has provider token or env vars are set. */ export function getLinearClientSync(context?: ToolContext): LinearClient { // 1. Try provider token from context const providerToken = context?.providerToken || context?.provider?.accessToken; if (providerToken) { const cacheKey = tokenToCacheKey('provider', providerToken); const cached = clientCache.get(cacheKey); if (cached) { return cached; } const client = createClient(providerToken); clientCache.set(cacheKey, client); return client; } // 2. Try direct bearer token (assume Linear token) const authHeader = context?.authHeaders?.authorization; if (authHeader) { const bearerMatch = authHeader.match(/^\s*Bearer\s+(.+)$/i); const token = bearerMatch?.[1]; if (token) { const cacheKey = tokenToCacheKey('bearer', token); const cached = clientCache.get(cacheKey); if (cached) { return cached; } const client = createClient(token); clientCache.set(cacheKey, client); return client; } } // 3. Fall back to environment variable (local dev only) const envAccessToken = config.LINEAR_ACCESS_TOKEN; if (!envAccessToken) { throw new Error( 'Linear OAuth required: complete the OAuth flow to get an access token', ); } const cacheKey = tokenToCacheKey('env', envAccessToken); const cached = clientCache.get(cacheKey); if (cached) { return cached; } const client = createClient(envAccessToken); clientCache.set(cacheKey, client); return client; } /** * Clear the client cache (useful for testing or token refresh). */ export function clearClientCache(): void { clientCache.clear(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/iceener/linear-streamable-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server