Skip to main content
Glama

Cozi MCP Server

index.ts8.17 kB
/** * Cozi MCP Server - Cloudflare Worker * * Implements Model Context Protocol for Cozi integration using Streamable HTTP transport * Allows Claude Desktop to interact with Cozi lists via MCP * * Transport: Streamable HTTP with optional SSE * - Single endpoint for all MCP communication * - Supports both standard JSON and SSE streaming responses */ import type { Env } from './types'; import { validateMCPToken, getCoziCredentials } from './auth'; import { CoziClient } from './cozi-client'; import { COZI_TOOLS, executeTool } from './tools'; import { createSSEStream, createSSEResponse, createJSONResponse, sendJSONRPCMessage, } from './sse-transport'; interface MCPRequest { jsonrpc: '2.0'; id?: string | number; method: string; params?: Record<string, any>; } interface MCPResponse { jsonrpc: '2.0'; id?: string | number; result?: any; error?: { code: number; message: string; data?: any; }; } export default { async fetch(request: Request, env: Env): Promise<Response> { console.log('🔵 MCP Request received:', { method: request.method, url: request.url, headers: Object.fromEntries(request.headers), }); // CORS headers const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization, Accept', }; if (request.method === 'OPTIONS') { console.log('✅ CORS preflight response'); return new Response(null, { headers: corsHeaders }); } // Handle GET requests if (request.method === 'GET') { const url = new URL(request.url); // OAuth 2.0 Protected Resource Metadata (RFC 9728) // This tells Claude.ai where our authorization server is if (url.pathname === '/.well-known/oauth-protected-resource') { console.log('🔐 OAuth Protected Resource Metadata request'); return new Response(JSON.stringify({ resource: url.origin, authorization_servers: [env.BRANDCAST_API_URL], scopes_supported: ['mcp:access'], bearer_methods_supported: ['header'], }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json', }, }); } // Root GET - return server info console.log('🔍 GET request - returning server info'); return new Response(JSON.stringify({ name: 'Cozi MCP Server', version: '1.0.0', description: 'Model Context Protocol server for Cozi integration', authorizationServer: env.BRANDCAST_API_URL, wellKnown: `${env.BRANDCAST_API_URL}/.well-known/oauth-authorization-server`, }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json', }, }); } try { // Parse MCP request (POST only) const mcpRequest = await request.json() as MCPRequest; console.log('📨 MCP Request parsed:', { method: mcpRequest.method, id: mcpRequest.id, hasParams: !!mcpRequest.params, }); // Handle initialize WITHOUT authentication (required for OAuth discovery) if (mcpRequest.method === 'initialize') { console.log('🚀 Initialize request - returning OAuth metadata:', { authorizationServer: env.BRANDCAST_API_URL, }); return createJSONResponse({ jsonrpc: '2.0', id: mcpRequest.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, }, serverInfo: { name: '@brandcast/cozi-mcp-server', version: '1.0.0', }, meta: { // This is how Claude discovers our OAuth endpoints! authorizationServer: env.BRANDCAST_API_URL, // e.g., https://dev-api.familycast.app }, }, }, 200, corsHeaders); } // All other methods require authentication const authHeader = request.headers.get('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return createJSONResponse({ jsonrpc: '2.0', error: { code: -32001, message: 'Missing or invalid Authorization header', }, }, 401, corsHeaders); } const token = authHeader.substring(7); const jwtPayload = await validateMCPToken(token, env); // Check if client requests streaming (SSE) const acceptHeader = request.headers.get('Accept') || ''; const useStreaming = acceptHeader.includes('text/event-stream'); // Handle MCP methods let response: MCPResponse; switch (mcpRequest.method) { case 'tools/list': { response = { jsonrpc: '2.0', id: mcpRequest.id, result: { tools: COZI_TOOLS, }, }; break; } case 'tools/call': { const toolName = mcpRequest.params?.name; const toolArgs = mcpRequest.params?.arguments || {}; if (!toolName) { response = { jsonrpc: '2.0', id: mcpRequest.id, error: { code: -32602, message: 'Missing tool name', }, }; break; } // Fetch Cozi credentials from BrandCast vault const credentials = await getCoziCredentials(jwtPayload.sub, env); // Extract accountId from metadata const accountId = credentials.metadata?.accountId; if (!accountId) { response = { jsonrpc: '2.0', id: mcpRequest.id, error: { code: -32000, message: 'Cozi account ID not found in credentials metadata', }, }; break; } // Create Cozi client and execute tool const coziClient = new CoziClient(credentials.accessToken, accountId); const toolResult = await executeTool(toolName, toolArgs, coziClient); response = { jsonrpc: '2.0', id: mcpRequest.id, result: { content: [ { type: 'text', text: JSON.stringify(toolResult, null, 2), }, ], }, }; break; } case 'ping': { response = { jsonrpc: '2.0', id: mcpRequest.id, result: {}, }; break; } default: { response = { jsonrpc: '2.0', id: mcpRequest.id, error: { code: -32601, message: `Method not found: ${mcpRequest.method}`, }, }; } } // Return response based on streaming preference if (useStreaming) { // SSE streaming response const { stream, send, close } = createSSEStream(); // Send the MCP response via SSE await sendJSONRPCMessage(send, response); // Close the stream (for single-message responses) // In a real streaming scenario, we'd keep this open for multiple messages await close(); return createSSEResponse(stream, corsHeaders); } else { // Standard JSON response return createJSONResponse(response, 200, corsHeaders); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorStack = error instanceof Error ? error.stack : undefined; console.error('❌ MCP Server Error:', { message: errorMessage, stack: errorStack, url: request.url, method: request.method, }); return createJSONResponse({ jsonrpc: '2.0', error: { code: -32000, message: errorMessage, }, }, 500, corsHeaders); } }, };

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/BrandCast-Signage/cozi-mcp-server'

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