UNPKG

@ankimcp/anki-mcp-server

Version:

Model Context Protocol server for Anki - enables AI assistants to interact with your Anki flashcards

266 lines • 10.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.deriveDashboardUrl = deriveDashboardUrl; exports.handleTunnel = handleTunnel; const core_1 = require("@nestjs/core"); const common_1 = require("@nestjs/common"); const app_module_1 = require("../../app.module.js"); const tunnel_1 = require("../index.js"); const tunnel_transport_1 = require("../tunnel.transport.js"); const app_config_service_1 = require("../../app-config.service.js"); const config_1 = require("../../config/index.js"); const cli_output_1 = require("../../cli/cli-output.js"); const spinner_1 = require("../../cli/spinner.js"); const bootstrap_1 = require("../../bootstrap.js"); const perform_login_1 = require("./perform-login"); function displayBox(cli, title, url) { cli.box(title, url); } function deriveDashboardUrl(tunnelUrl) { const url = new URL(tunnelUrl); url.hostname = url.hostname.replace(/^tunnel\./, "web."); return url.origin; } function printTunnelBoxes(cli, tunnelUrl) { displayBox(cli, "🚇 Tunnel URL", tunnelUrl); cli.blank(); displayBox(cli, "🌐 Dashboard", deriveDashboardUrl(tunnelUrl)); cli.blank(); } function formatConnectionError(error, tunnelUrl) { if (error instanceof tunnel_1.TunnelClientError && error.originalError) { const origError = error.originalError; if (origError.code === "ECONNREFUSED") { return `Cannot connect to tunnel server at ${tunnelUrl} Make sure the tunnel server is running.`; } } if (error instanceof Error) { if (error.message.includes("ECONNREFUSED") || error.message.includes("connect ECONNREFUSED")) { return `Cannot connect to tunnel server at ${tunnelUrl} Make sure the tunnel server is running.`; } if (error.message.includes("timeout") || error.message.includes("Connection timeout")) { return `Connection timeout to ${tunnelUrl} The tunnel server may be unavailable or slow to respond.`; } if (error.message.includes("Unauthorized") || error.message.includes("401")) { return `Authentication failed Your credentials may be invalid. Try: ankimcp --logout && ankimcp --login`; } } const message = error instanceof Error ? error.message : String(error); return `Failed to connect: ${message}`; } async function ensureCredentials(cli, credentialsService, deviceFlowService) { const existing = await credentialsService.loadCredentials(); if (existing) { return existing; } if (!process.stdout.isTTY) { cli.error("Not logged in and running non-interactively. Run: ankimcp --login first."); process.exit(1); } cli.info("Not logged in — starting authentication flow..."); cli.blank(); try { const credentials = await (0, perform_login_1.performLogin)({ credentialsService, deviceFlowService, cli, }); cli.info("Continuing to tunnel..."); cli.blank(); return credentials; } catch (error) { cli.blank(); (0, perform_login_1.reportLoginError)(cli, error); cli.blank(); process.exit(1); } } async function handleTunnel(cli, tunnelUrl, debug, readOnly, ankiConnect, createMicroservice = (module, options) => core_1.NestFactory.createMicroservice(module, options)) { const credentialsService = new tunnel_1.CredentialsService(); const validatedConfig = (0, config_1.loadValidatedConfig)({ debug, readOnly, tunnel: tunnelUrl, ankiConnect, }); const appConfigService = new app_config_service_1.AppConfigService(validatedConfig); const deviceFlowService = new tunnel_1.DeviceFlowService(appConfigService); let app; let tunnelClient; let shuttingDown = false; const gracefulExit = async (code, message) => { if (shuttingDown) { process.exit(code); } shuttingDown = true; try { if (message) { cli.info(message); } tunnelClient?.disconnect(); if (app) { await app.close(); } } catch (err) { cli.error(`Cleanup error: ${err instanceof Error ? err.message : String(err)}`); } process.exit(code); }; cli.info((0, cli_output_1.formatBanner)("Tunnel")); cli.blank(); try { const credentials = await ensureCredentials(cli, credentialsService, deviceFlowService); const stopSpinner = (0, spinner_1.startSpinner)("Starting MCP service..."); const mcpTransport = new tunnel_transport_1.TunnelTransport(); const mcp = (0, bootstrap_1.createMcpStrategy)([mcpTransport], validatedConfig.mcpServer); const logLevel = debug ? "debug" : "info"; const pinoLogger = (0, bootstrap_1.createPinoLogger)(bootstrap_1.LOG_DESTINATION.STDERR, logLevel); const loggerService = (0, bootstrap_1.createLoggerService)(pinoLogger); if (debug) { common_1.Logger.overrideLogger(["log", "error", "warn", "debug", "verbose"]); } common_1.Logger.overrideLogger(loggerService); try { app = await createMicroservice(app_module_1.AppModule.forTunnel(mcp, { debug, readOnly, ankiConnect }), { strategy: mcp, logger: loggerService, }); await app.listen(); stopSpinner(); cli.success("MCP service ready"); cli.info(`🔌 AnkiConnect URL: ${validatedConfig.ankiConnect.url}`); } catch (error) { stopSpinner(); cli.error(`Failed to start MCP service: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined); cli.blank(); return await gracefulExit(1); } cli.blank(); const mcpHandler = { async handle(request) { const responseBody = await mcpTransport.handleRequest(request.body); return { status: 200, headers: { "content-type": "application/json" }, body: responseBody, }; }, }; const effectiveTunnelUrl = tunnelUrl ?? validatedConfig.tunnel.serverUrl; tunnelClient = new tunnel_1.TunnelClient(mcpHandler, credentialsService, deviceFlowService, effectiveTunnelUrl); tunnelClient.on("error", (error) => { if (error instanceof tunnel_1.TunnelClientError && error.code === "session_expired") { cli.blank(); cli.error(error.message); void gracefulExit(1); return; } if (tunnelClient?.isConnected()) { cli.error(`Tunnel error: ${error.message}`, error); } }); let connectSpinner = (0, spinner_1.startSpinner)("Connecting to tunnel service..."); let publicUrl; let connectCredentials = credentials; let reloginAttempted = false; for (;;) { try { publicUrl = await tunnelClient.connect(connectCredentials); connectSpinner(); cli.success("Tunnel established"); cli.blank(); break; } catch (error) { connectSpinner(); const isSessionExpired = error instanceof tunnel_1.TunnelClientError && error.code === "session_expired"; if (isSessionExpired && !reloginAttempted && process.stdout.isTTY) { reloginAttempted = true; cli.blank(); cli.info("Session expired — re-authenticating..."); cli.blank(); let freshCredentials; try { freshCredentials = await (0, perform_login_1.performLogin)({ credentialsService, deviceFlowService, cli, }); } catch (loginError) { cli.blank(); (0, perform_login_1.reportLoginError)(cli, loginError); cli.blank(); return await gracefulExit(1); } connectCredentials = freshCredentials; connectSpinner = (0, spinner_1.startSpinner)("Connecting to tunnel service..."); continue; } if (isSessionExpired) { cli.blank(); cli.error(error.message); return await gracefulExit(1); } const errorMessage = formatConnectionError(error, effectiveTunnelUrl); cli.error(errorMessage, error instanceof Error ? error : undefined); cli.blank(); return await gracefulExit(1); } } printTunnelBoxes(cli, publicUrl); cli.info("Tunnel is active. Press Ctrl+C to disconnect."); cli.blank(); const logger = new common_1.Logger("TunnelCommand"); tunnelClient.on("request", (requestId, request) => { logger.log(`Request ${requestId}: ${request.method} ${request.path}`); }); tunnelClient.on("disconnected", (code, reason, willReconnect) => { cli.blank(); if (willReconnect) { cli.warn(`Tunnel disconnected (code ${code}): ${reason}. Reconnecting…`); } else { cli.error(`Tunnel disconnected (code ${code}): ${reason}`); } cli.blank(); }); let activeTunnelUrl = publicUrl; tunnelClient.on("tunnel_url", (url) => { if (url === activeTunnelUrl) { return; } activeTunnelUrl = url; cli.blank(); cli.info("Tunnel URL changed after reconnect:"); printTunnelBoxes(cli, url); }); process.on("SIGINT", () => { cli.blank(); void gracefulExit(0, "Shutting down..."); }); process.on("SIGTERM", () => { cli.blank(); void gracefulExit(0, "Shutting down..."); }); await new Promise(() => { }); } catch (error) { cli.error(`Tunnel failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined); cli.blank(); await gracefulExit(1); } } //# sourceMappingURL=tunnel.command.js.map