import { kody } from 'kody:runtime' export const GRANOLA_MCP_URL = 'https://mcp.granola.ai/mcp' export const DEFAULT_MCP_SERVER = 'granola' export const KODY_MCP_PAGE = 'https://kody.codes/account/mcp-servers' /** Tools documented at https://docs.granola.ai/help-center/sharing/integrations/mcp (all read-only). */ export const GRANOLA_MCP_TOOLS = [ 'query_granola_meetings', 'list_meetings', 'get_meetings', 'list_meeting_folders', 'get_meeting_transcript', 'get_account_info', ] as const export const MCP_SERVER_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/ type ServerInfo = { id?: string; name?: string; url?: string; state?: string; status?: string; authUrl?: string | null; tools?: Array; toolNames?: Array; error?: string | null } & Record export async function findMcpServer(name: string): Promise { const listed = (await kody.mcpServerList({})) as unknown const servers: Array = Array.isArray(listed) ? (listed as Array) : listed && typeof listed === 'object' && Array.isArray((listed as { servers?: unknown }).servers) ? (listed as { servers: Array }).servers : [] return servers.find((s) => s.name === name) ?? null } export function toolNamesOf(server: ServerInfo): Array { if (Array.isArray(server.toolNames)) return server.toolNames.map(String) if (Array.isArray(server.tools)) return server.tools.map((t) => (typeof t === 'string' ? t : String((t as { name?: string })?.name ?? ''))).filter(Boolean) return [] } function notConnectedError(server: string, tool: string, cause?: unknown): Error { const why = cause ? ` (Kody said: ${String((cause as Error)?.message ?? cause).slice(0, 200)})` : '' return new Error( `MCP server "${server}" is not connected in this Kody account (or has no tool "${tool}")${why}. One-time setup: open ${KODY_MCP_PAGE}, add a server named ` + `"${server}" with URL ${GRANOLA_MCP_URL}, and finish the Granola sign-in from the authorization link (or run ./mcp/connect({ confirm: true }) and open its authUrl). Check status with ./mcp/tools.`, ) } /** Kody's MCP hub reports a missing server or tool with messages like 'Unknown MCP server "x"'. */ const NOT_CONNECTED = /unknown mcp server|unknown tool|no such (mcp )?server|not connected|no mcp server/i const NEEDS_AUTH = /authoriz|re-?auth|oauth|disconnected|refresh token/i export async function callMcpTool(server: string, tool: string, args: Record): Promise { const hub = (kody as unknown as { mcp?: Record Promise>> }).mcp let fn: unknown try { fn = hub?.[server]?.[tool] } catch (e) { throw notConnectedError(server, tool, e) } if (typeof fn !== 'function') throw notConnectedError(server, tool) try { return await (fn as (a: unknown) => Promise).call(hub?.[server], args) } catch (e) { const msg = String((e as Error)?.message ?? e) if (NOT_CONNECTED.test(msg)) throw notConnectedError(server, tool, e) if (NEEDS_AUTH.test(msg)) { throw new Error( `Granola MCP server "${server}" needs you to sign in again (Kody said: ${msg.slice(0, 200)}). Reconnect it from ${KODY_MCP_PAGE} (open the server, Reconnect, then sign in to Granola), then retry.`, ) } throw e } } /** Flatten an MCP tool result (`{ content: [{ type: 'text', text }], isError }`, structured, or plain) to text. */ export function mcpText(result: unknown): { text: string; isError: boolean } { if (typeof result === 'string') return { text: result, isError: false } if (result && typeof result === 'object') { const r = result as { content?: Array<{ type?: string; text?: string }>; isError?: boolean; structuredContent?: unknown; result?: unknown } if (Array.isArray(r.content)) { const text = r.content.filter((c) => c?.type === 'text' || typeof c?.text === 'string').map((c) => c.text ?? '').join('\n') return { text, isError: Boolean(r.isError) } } if (r.result !== undefined) return mcpText(r.result) if (r.structuredContent !== undefined) return { text: JSON.stringify(r.structuredContent), isError: Boolean(r.isError) } } return { text: JSON.stringify(result ?? null), isError: false } } /** Citation links Granola puts in answers, e.g. `[[0]](https://notes.granola.ai/d/…)` or `[0](…)`. */ export function extractCitations(text: string): Array<{ ref: string; url: string }> { const out: Array<{ ref: string; url: string }> = [] const seen = new Set() const re = /\[\[?(\d+)\]?\]\((https?:\/\/[^)\s]+)\)/g let m: RegExpExecArray | null while ((m = re.exec(text))) { const key = `${m[1]}|${m[2]}` if (seen.has(key)) continue seen.add(key) out.push({ ref: m[1] as string, url: m[2] as string }) } return out }