Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cute-dogs-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@deablabs/cannoli-server": minor
---

feat: Improve logging. Display password on first startup.
3 changes: 3 additions & 0 deletions packages/cannoli-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"typescript": "^5.8.3"
},
"dependencies": {
"@clack/core": "0.5.0",
"@clack/prompts": "0.11.0",
"@hono/node-server": "^1.14.0",
"@hono/zod-validator": "0.4.3",
"@modelcontextprotocol/sdk": "1.9.0",
Expand All @@ -57,6 +59,7 @@
"hono-openapi": "0.4.6",
"mcp-proxy": "2.12.0",
"nanoid": "5.0.7",
"picocolors": "1.1.1",
"raw-body": "3.0.0",
"remeda": "1.61.0",
"tiny-invariant": "^1.3.1",
Expand Down
89 changes: 89 additions & 0 deletions packages/cannoli-server/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { log } from "@clack/prompts";
import { argumentsConfig } from "src/arguments";

declare global {
// eslint-disable-next-line no-var
var logger: Logger | undefined;
}

type LoggerLevel = "info" | "warn" | "error" | "debug";

export type Logger = {
getLevel: () => LoggerLevel;
setLevel: (level: LoggerLevel) => void;
success: (message: string, ...rest: string[]) => void;
log: (message: string, ...rest: string[]) => void;
warn: (message: string, ...rest: string[]) => void;
error: (message: string, ...rest: string[]) => void;
debug: (message: string, ...rest: string[]) => void;
};

function ensureLogger(level: LoggerLevel): Logger {
if (!globalThis.logger) {
globalThis.logger = new ClackLogger(level);
}
return globalThis.logger;
}

export const getLogger = (
level: LoggerLevel = argumentsConfig.verbose ? "debug" : "info",
): Logger => ensureLogger(level);

export class ClackLogger implements Logger {
private level: LoggerLevel;

constructor(level: LoggerLevel) {
this.level = level;
}

getLevel(): LoggerLevel {
return this.level;
}

setLevel(level: LoggerLevel): void {
this.level = level;
}

private shouldLog(level: LoggerLevel): boolean {
const levels = { error: 0, warn: 1, info: 2, debug: 3 };
return levels[level] <= levels[this.level];
}

private concatenateMessages(message: string, ...rest: string[]): string {
if (rest.length === 0) {
return message;
}
return `${message} ${rest.join(" ")}`;
}

log = (message: string, ...rest: string[]): void => {
if (this.shouldLog("info")) {
log.info(this.concatenateMessages(message, ...rest));
}
};

success = (message: string, ...rest: string[]): void => {
if (this.shouldLog("info")) {
log.success(this.concatenateMessages(message, ...rest));
}
};

warn = (message: string, ...rest: string[]): void => {
if (this.shouldLog("warn")) {
log.warn(this.concatenateMessages(message, ...rest));
}
};

error = (message: string, ...rest: string[]): void => {
if (this.shouldLog("error")) {
log.error(this.concatenateMessages(message, ...rest));
}
};

debug = (message: string, ...rest: string[]): void => {
if (this.shouldLog("debug")) {
// Clack doesn't have a debug method, use info with a prefix
log.info(`[DEBUG] ${this.concatenateMessages(message, ...rest)}`);
}
};
}
26 changes: 15 additions & 11 deletions packages/cannoli-server/src/routes/sse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import { SSEServerTransport } from "src/impl/SSEServerTransport";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { EventEmitter } from "events";
import { getLogger } from "src/logger";

// 4mb in kb
const MAXIMUM_MESSAGE_SIZE = 4 * 1024 * 1024;

// Map of sessionId to SSEServerTransport
const ActiveSSEConnections = new Map<string, SSEServerTransport>();

const logger = getLogger();

const router = new Hono()
.get("/ping", async (c: Context) => {
return c.text("pong");
Expand Down Expand Up @@ -49,7 +52,7 @@ const router = new Hono()
env: process.env as Record<string, string>,
stderr: "pipe",
onEvent: (event) => {
console.log("transport event", event);
logger.debug(`transport event\n${event}`);
},
});

Expand Down Expand Up @@ -105,7 +108,7 @@ const router = new Hono()
stream,
);
sseServerTransport.onerror = (error) => {
console.error("SSE server transport error", error);
logger.error(`SSE server transport error\n${error}`);
};
ActiveSSEConnections.set(
sseServerTransport.sessionId,
Expand All @@ -114,14 +117,14 @@ const router = new Hono()

try {
await mcpServer.connect(sseServerTransport);

await sseServerTransport.send({
jsonrpc: "2.0",
method: "sse/connection",
params: { message: "SSE Connection established" },
});
logger.log("Serving connection to MCP server:", server.name);
} catch (error) {
console.error("Error sending SSE connection message", error);
logger.error(`Error sending SSE connection message\n${error}`);
if (!closed) {
stream.writeSSE({
event: "error",
Expand All @@ -132,21 +135,22 @@ const router = new Hono()

// handle abort event from client side
c.req.raw.signal.addEventListener("abort", () => {
console.log("stream disconnected from client side");
logger.debug("stream disconnected from client side");
emitter.emit("close");
});

// keep the connection alive
return new Promise((resolve) => {
console.log("waiting for close");
logger.debug("waiting for close");
const abort = async () => {
console.log("stream aborted");
logger.debug("stream aborted");
try {
await mcpServer.close();
await client.close();
await serverTransport.close();
logger.log("Closed connection to MCP server:", server.name);
} catch (error) {
console.error("Error closing mcp server", error);
logger.error(`Error closing mcp server\n${error}`);
}
ActiveSSEConnections.delete(requestId);
resolve(undefined);
Expand All @@ -167,19 +171,19 @@ const router = new Hono()
const server = await getServerById(configDir, serverId);

if (!server) {
console.error("Server not found", serverId);
logger.error(`Server not found\n${serverId}`);
return c.json({ error: "Server not found" }, 404);
}

if (server.type !== "stdio") {
console.error("Server is not a stdio server", serverId);
logger.error(`Server is not a stdio server\n${serverId}`);
return c.text("Server is not a stdio server", 400);
}

const sseServerTransport = ActiveSSEConnections.get(sessionId);

if (!sseServerTransport) {
console.error("SSE connection not found", sessionId);
logger.error(`SSE connection not found\n${sessionId}`);
return c.text("SSE connection not found", 404);
}

Expand Down
56 changes: 34 additions & 22 deletions packages/cannoli-server/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { logger } from "hono/logger";
import { serve, type HttpBindings } from "@hono/node-server";
import { cors } from "hono/cors";
import { logger as honoLogger } from "hono/logger";
import * as path from "node:path";
import { getConfigDir } from "./utils";
import { ServerInfo } from "./types";
Expand All @@ -17,22 +17,30 @@ import settingsRouter from "./routes/settings";
import sseRouter from "./routes/sse";
import { loadSettings } from "src/settings";
import { argumentsConfig, optionsDefinition } from "src/arguments";
import { getLogger, Logger } from "src/logger";
import { intro, outro } from "@clack/prompts";
import colors from "picocolors";

declare module "hono" {
interface ContextVariableMap {
configDir: string;
requestId: string;
host: string;
port: number;
logger: Logger;
}
}

intro(`${colors.bgBlueBright(colors.black(` Cannoli Server `))}`);

const logger = getLogger();

if (argumentsConfig.help) {
// generate help message from options config
const helpMessage = Object.entries(optionsDefinition)
.map(([key, value]) => `${key}: ${value.type}`)
.join("\n");
console.log(`
logger.log(`
Usage: cannoli-server [options]

Options:
Expand All @@ -41,11 +49,12 @@ ${helpMessage}
process.exit(0);
}

type Bindings = HttpBindings;

// Create the app
const app = new Hono();
if (argumentsConfig.verbose) {
app.use("*", logger());
}
const app = new Hono<{ Bindings: Bindings }>();
app.use(requestId());
app.use(honoLogger(logger.debug));

// Configuration
const HOST = argumentsConfig.host || process.env.HOST || "localhost";
Expand All @@ -71,16 +80,12 @@ app.use(async (...args) => {
});

// Middleware to add configDir to context
app.use(
"*",
(c, next) => {
c.set("configDir", CONFIG_DIR);
c.set("host", HOST);
c.set("port", Number(PORT));
return next();
},
requestId(),
);
app.use("*", (c, next) => {
c.set("configDir", CONFIG_DIR);
c.set("host", HOST);
c.set("port", Number(PORT));
return next();
});

// Mount the routers
const routerApp = app
Expand Down Expand Up @@ -121,6 +126,14 @@ app.get(
}),
);

// catch ctrl-c and exit gracefully
process.on("SIGINT", () => {
outro(
`${colors.bgRedBright(colors.white(` Shutting down Cannoli server... Bye! `))}`,
);
process.exit(0);
});

// Start server
serve(
{
Expand All @@ -129,11 +142,10 @@ serve(
hostname: HOST,
},
(info: ServerInfo) => {
console.log(`Cannoli server listening on http://${HOST}:${info.port}`);
console.log(`Using config file: ${SETTINGS_FILE}`);
console.log(
`API documentation available at http://${HOST}:${info.port}/docs`,
);
logger.log("Logger level :", logger.getLevel());
logger.log(`Using config file : ${SETTINGS_FILE}`);
logger.log(`API documentation : http://${HOST}:${info.port}/docs`);
logger.success(`Listening on : http://${HOST}:${info.port}`);
},
);

Expand Down
Loading