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
49 changes: 49 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# AGENTS.md

Instructions for AI coding agents working on this codebase.

## Logging

**Do not use `console.log`, `console.warn`, or `console.error` directly.**

### Server code (`server/`)

Use the centralized logger utility:

```typescript
import { logger } from "@/utils/logger";

logger.error("Something failed", error, { userId: "123" });
logger.warn("Deprecated API used", { endpoint: "/old" });
logger.info("Server started");
logger.debug("Processing request", requestData);
```

**Why?** The logger sends errors/warnings to Sentry and respects the `--verbose` flag (silent in production by default).

### CLI script (`bin/start.js`)

Use the built-in colored logging functions:

```javascript
logSuccess("Server started"); // ✅ green
logInfo("Using port 6274"); // ℹ️ blue
logWarning("Port in use"); // ⚠️ yellow
logError("Failed to start"); // ❌ red
logStep("Build", "Compiling..."); // [Build] cyan header
logProgress("Waiting..."); // ⏳ magenta
logDivider(); // ── dim line
logBox("http://localhost:6274", "MCPJam"); // boxed output
```

For verbose-only output (hidden unless `--verbose` flag):

```javascript
verboseInfo("Loading config...");
verboseSuccess("Config loaded");
verboseStep("Setup", "Initializing...");
```

### Client code

Browser `console.*` is acceptable for client-side debugging.
84 changes: 57 additions & 27 deletions bin/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ import open from "open";

const __dirname = dirname(fileURLToPath(import.meta.url));

const MCP_BANNER = `
███╗ ███╗ ██████╗██████╗ ██╗ █████╗ ███╗ ███╗
████╗ ████║██╔════╝██╔══██╗ ██║██╔══██╗████╗ ████║
██╔████╔██║██║ ██████╔╝ ██║███████║██╔████╔██║
██║╚██╔╝██║██║ ██╔═══╝██ ██║██╔══██║██║╚██╔╝██║
██║ ╚═╝ ██║╚██████╗██║ ╚█████╔╝██║ ██║██║ ╚═╝ ██║
╚═╝ ╚═╝ ╚═════╝╚═╝ ╚════╝ ╚═╝ ╚═╝╚═╝ ╚═╝
`;
// Banner split into MCP (white) and JAM (primary orange) parts
const MCP_BANNER_LINES = [
["███╗ ███╗ ██████╗██████╗", " ██╗ █████╗ ███╗ ███╗"],
["████╗ ████║██╔════╝██╔══██╗", " ██║██╔══██╗████╗ ████║"],
["██╔████╔██║██║ ██████╔╝", " ██║███████║██╔████╔██║"],
["██║╚██╔╝██║██║ ██╔═══╝", "██ ██║██╔══██║██║╚██╔╝██║"],
["██║ ╚═╝ ██║╚██████╗██║ ", "╚█████╔╝██║ ██║██║ ╚═╝ ██║"],
["╚═╝ ╚═╝ ╚═════╝╚═╝ ", "╚════╝ ╚═╝ ╚═╝╚═╝ ╚═╝"],
];

// ANSI color codes
// Theme colors from index.css (oklch converted to RGB)
const colors = {
reset: "\x1b[0m",
bright: "\x1b[1m",
Expand All @@ -31,6 +33,8 @@ const colors = {
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m",
primary: "\x1b[38;2;207;115;69m", // oklch(0.6832 0.1382 38.744) - orange
default: "\x1b[39m", // Default foreground - adapts to terminal theme
bgRed: "\x1b[41m",
bgGreen: "\x1b[42m",
bgYellow: "\x1b[43m",
Expand All @@ -44,6 +48,16 @@ function log(message, color = colors.reset) {
console.log(`${color}${message}${colors.reset}`);
}

function printBanner() {
console.log();
for (const [mcp, jam] of MCP_BANNER_LINES) {
console.log(
`${colors.default}${mcp}${colors.primary}${jam}${colors.reset}`,
);
}
console.log();
}

function logSuccess(message) {
log(`✅ ${message}`, colors.green);
}
Expand Down Expand Up @@ -80,7 +94,7 @@ function logBox(content, title = null) {
const maxLength = Math.max(...lines.map((line) => line.length));
const width = maxLength + 4;

log("┌" + "─".repeat(width) + "┐", colors.cyan);
log("┌" + "─".repeat(width) + "┐", colors.primary);
if (title) {
const titlePadding = Math.floor((width - title.length - 2) / 2);
log(
Expand All @@ -89,17 +103,17 @@ function logBox(content, title = null) {
title +
" ".repeat(width - title.length - titlePadding) +
"│",
colors.cyan,
colors.primary,
);
log("├" + "─".repeat(width) + "┤", colors.cyan);
log("├" + "─".repeat(width) + "┤", colors.primary);
}

lines.forEach((line) => {
const padding = width - line.length - 2;
log("│ " + line + " ".repeat(padding) + " │", colors.cyan);
log("│ " + line + " ".repeat(padding) + " │", colors.primary);
});

log("└" + "─".repeat(width) + "┘", colors.cyan);
log("└" + "─".repeat(width) + "┘", colors.primary);
}

function delay(ms) {
Expand Down Expand Up @@ -292,8 +306,7 @@ async function setupOllamaInSingleTerminal(model) {
async function main() {
// Show MCP banner at startup
console.clear();
log(MCP_BANNER, colors.cyan);
logDivider();
printBanner();

// Parse command line arguments
const args = process.argv.slice(2);
Expand All @@ -313,6 +326,20 @@ async function main() {
let bearerToken = null;
let useOAuth = false;
const customHeaders = [];
let verboseLogs = false;

// First pass: check for --verbose flag before processing other args
for (const arg of args) {
if (arg === "--verbose" || arg === "-v") {
verboseLogs = true;
break;
}
}

// Conditional logging functions (only log when verbose)
const verboseInfo = (message) => verboseLogs && logInfo(message);
const verboseSuccess = (message) => verboseLogs && logSuccess(message);
const verboseStep = (step, message) => verboseLogs && logStep(step, message);

Comment on lines +329 to 343
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Verbose logging may leak secrets via custom header values; redact by key.
Even in verbose mode, printing raw header values (especially cookies/tokens) is risky in shared terminals/log captures.

+  const redactHeaderValue = (key, value) =>
+    /authorization|cookie|token|secret|api[-_]?key/i.test(String(key))
+      ? "<redacted>"
+      : value;
+
   for (const { key, value } of customHeaders) {
     headers[key] = value;
-    verboseInfo(`Header: ${key}=${value}`);
+    verboseInfo(`Header: ${key}=${redactHeaderValue(key, value)}`);
   }

Also applies to: 421-425, 549-552

for (let i = 0; i < args.length; i++) {
const arg = args[i];
Expand Down Expand Up @@ -391,6 +418,12 @@ async function main() {
continue;
}

// New: --verbose flag to enable HTTP request logs in production
if (parsingFlags && (arg === "--verbose" || arg === "-v")) {
verboseLogs = true;
continue;
}

// New: --header for custom headers (repeatable)
if (
parsingFlags &&
Expand Down Expand Up @@ -496,8 +529,8 @@ async function main() {
} else if (httpUrl) {
// Handle HTTP URL mode (for Vite plugin integration, etc.)
const displayName = serverDisplayName || "HTTP Server";
logStep("MCP Server", `Configuring HTTP server: ${displayName}`);
logInfo(`URL: ${httpUrl}`);
verboseStep("MCP Server", `Configuring HTTP server: ${displayName}`);
verboseInfo(`URL: ${httpUrl}`);

// Validate the URL
try {
Expand All @@ -511,11 +544,11 @@ async function main() {
const headers = {};
if (bearerToken) {
headers["Authorization"] = `Bearer ${bearerToken}`;
logInfo("Auth: Bearer token configured");
verboseInfo("Auth: Bearer token configured");
}
for (const { key, value } of customHeaders) {
headers[key] = value;
logInfo(`Header: ${key}=${value}`);
verboseInfo(`Header: ${key}=${value}`);
}

// Create a synthetic MCP config for the HTTP server
Expand All @@ -532,9 +565,9 @@ async function main() {
envVars.MCP_CONFIG_DATA = JSON.stringify(httpServerConfig);
envVars.MCP_AUTO_CONNECT_SERVER = displayName;
if (useOAuth) {
logInfo("OAuth: Will trigger OAuth flow on connect");
verboseInfo("OAuth: Will trigger OAuth flow on connect");
}
logSuccess(`HTTP server "${displayName}" configured for auto-connect`);
verboseSuccess(`HTTP server "${displayName}" configured for auto-connect`);
} else if (mcpServerCommand) {
// Handle single MCP server command if provided (legacy mode)
logStep(
Expand All @@ -554,7 +587,7 @@ async function main() {
// Pass global options (applicable to all modes)
if (initialTab) {
envVars.MCP_INITIAL_TAB = initialTab;
logInfo(`Initial tab: ${initialTab}`);
verboseInfo(`Initial tab: ${initialTab}`);
}

// Handle Ollama setup if requested
Expand Down Expand Up @@ -599,10 +632,8 @@ async function main() {
const hasExplicitPort = envVars.PORT !== undefined;

if (hasExplicitPort) {
logInfo(`Using explicitly requested port: ${requestedPort}`);
if (await isPortAvailable(requestedPort)) {
PORT = requestedPort.toString();
logSuccess(`Port ${requestedPort} is available and ready`);
} else {
logError(`Explicitly requested port ${requestedPort} is not available`);
logInfo(
Expand All @@ -611,11 +642,9 @@ async function main() {
throw new Error(`Port ${requestedPort} is already in use`);
}
} else {
// Fixed port policy: use default port 3000 and fail fast if unavailable
logInfo("No specific port requested, using fixed default port 6274");
// Fixed port policy: use default port 6274 and fail fast if unavailable
if (await isPortAvailable(requestedPort)) {
PORT = requestedPort.toString();
logSuccess(`Default port ${requestedPort} is available`);
} else {
logError(
`Default port ${requestedPort} is already in use. Please free the port`,
Expand Down Expand Up @@ -689,6 +718,7 @@ async function main() {
...process.env,
NODE_ENV: "production",
PORT: PORT,
...(verboseLogs && { VERBOSE_LOGS: "true" }),
},
cwd: projectRoot,
stdio: "inherit",
Expand Down
Loading