@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
JavaScript
;
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