Server Logs
Real-time log streaming architecture on mctx. How logs are captured, WebSocket streaming, console output, and no retention policy.
mctx provides real-time log streaming so developers can debug and monitor their MCP servers. Logs stream live via WebSocket with zero configuration.
Architecture
Real-time logging uses two components:
| Component | Purpose | Technology |
|---|---|---|
| Tail Worker | Captures console output | Dispatch namespace tail consumer |
| LogStream Durable Object | Maintains WebSocket connections | Cloudflare Durable Object |
Log Flow
- Tenant worker calls
console.log()during request handling - Tail worker receives log event from dispatch namespace
- Tail worker extracts tenant slug from
X-Tenant-Slugheader - Tail worker POSTs batched logs to LogStream DO for that slug
- LogStream DO broadcasts to all connected WebSocket clients
- Dashboard modal displays logs in real-time
Header Injection
The dispatch worker adds X-Tenant-Slug header when forwarding to tenant workers:
// Dispatch worker
const tenantRequest = new Request(request, {
headers: {
...request.headers,
"X-Tenant-Slug": slug,
},
});This allows the tail worker to associate logs with the correct server.
What Gets Logged
Any console.* output from your server code:
| Method | Level | Use Case |
|---|---|---|
console.log() | Info | General operational events |
console.info() | Info | Informational messages |
console.warn() | Warn | Warnings and potential issues |
console.error() | Error | Errors and failures |
console.debug() | Debug | Detailed debugging info |
Example
export default {
async fetch(request: Request, env: Env): Promise<Response> {
console.log(`[INFO] ${request.method} ${request.url}`);
try {
const result = await processRequest(request, env);
console.log(`[INFO] Request processed successfully`);
return result;
} catch (error) {
console.error(`[ERROR] Request failed: ${error.message}`);
throw error;
}
},
};Log Format
Logs are captured with metadata:
interface LogEntry {
timestamp: number; // Unix timestamp in milliseconds
level: "log" | "info" | "warn" | "error" | "debug";
message: string; // The logged message
}Dashboard Display
Logs appear in the dashboard modal with:
- Timestamp - Local time (HH:MM:SS.mmm)
- Level badge - Color-coded (info/warn/error)
- Message - Full log message
Auto-Scroll
The log viewer auto-scrolls to the latest message. Scrolling up pauses auto-scroll until you scroll to the bottom again.
Accessing Logs
Via Dashboard
- Navigate to server detail page (
/dev/servers/[id]) - Click View Logs button in Server Information section
- Modal opens showing real-time log stream
- Trigger requests to see logs appear
Requirement: Server must be in Active status. Logs are not available for inactive servers.
WebSocket Connection
The dashboard connects via WebSocket:
wss://workers.mctx.ai/log-stream/{slug}Authentication: Requires valid session cookie from authenticated user who owns the server.
Connection Lifecycle
- Connect - Open WebSocket connection to LogStream DO
- Subscribe - LogStream DO registers client for that server slug
- Stream - Receive log entries as they arrive
- Disconnect - Close modal, WebSocket disconnects
No Retention
Critical: Logs are not stored. They are ephemeral and only visible while streaming.
Implications:
- Cannot view past logs after closing modal
- Cannot search historical logs
- Cannot export logs
- No log retention policy
For persistent logging: Integrate with external logging service (Datadog, New Relic, Logtail, etc.) by sending logs from your server code.
Log Streaming Best Practices
Use Structured Prefixes
// Consistent prefix format
console.log(`[INFO] Tool called: ${toolName}`);
console.log(`[DEBUG] Parameters: ${JSON.stringify(params)}`);
console.log(`[WARN] Rate limit approaching: ${remaining} requests left`);
console.log(`[ERROR] API call failed: ${error.message}`);Log Important Events
// Request lifecycle
console.log(`[INFO] ${request.method} request received`);
console.log(`[INFO] Tool: ${toolName}, Params: ${JSON.stringify(params)}`);
console.log(`[INFO] External API call: ${apiUrl}`);
console.log(`[INFO] Response sent: ${response.status}`);
// Errors
console.error(`[ERROR] Failed to parse request: ${error.message}`);
console.error(
`[ERROR] API returned ${response.status}: ${await response.text()}`,
);Avoid Logging Secrets
// ❌ Bad - Exposes secret
console.log(`Using API key: ${env.API_KEY}`);
// ✅ Good - Safe reference
console.log(`Using API key: ${env.API_KEY.slice(0, 4)}...`);Add Timing Information
const start = Date.now();
const result = await callExternalApi(query, env);
const duration = Date.now() - start;
console.log(`[INFO] API call completed in ${duration}ms`);Log Level Guidelines
| Level | When to Use |
|---|---|
DEBUG | Detailed information for troubleshooting |
INFO | Normal operational events |
WARN | Something unexpected but not an error |
ERROR | Error conditions that should be investigated |
Troubleshooting with Logs
Request Not Reaching Server
Symptoms: No logs appear when request should be processed
Check:
- Is the log modal open? (Logs only stream while connected)
- Is request going to correct version endpoint?
- Is authentication working? (Check for auth errors in browser console)
Solution: Verify endpoint URL and ensure OAuth token is valid.
Request Processing Errors
Symptoms: Error logs appearing
Check:
- Error message content
- Stack trace (if logged)
- Which tool or operation failed
Example:
try {
const data = await fetchData(url, env.API_KEY);
} catch (error) {
console.error(`[ERROR] Fetch failed: ${error.message}`);
console.error(`[ERROR] URL: ${url}`);
console.error(`[ERROR] Stack: ${error.stack}`);
throw error;
}Slow Responses
Symptoms: Requests take too long
Check: Add timing logs to identify bottlenecks
console.log(`[INFO] Request started`);
const t1 = Date.now();
const data = await fetchExternalApi(query);
console.log(`[INFO] External API: ${Date.now() - t1}ms`);
const result = await processData(data);
console.log(`[INFO] Processing: ${Date.now() - t1}ms total`);Missing Environment Variables
Symptoms: Code fails when accessing env.*
Check: Log environment variable presence (not values!)
console.log(`[DEBUG] API_KEY present: ${!!env.API_KEY}`);
console.log(`[DEBUG] DATABASE_URL present: ${!!env.DATABASE_URL}`);
if (!env.API_KEY) {
console.error(`[ERROR] API_KEY not configured`);
throw new Error("Server misconfigured");
}Log Volume Considerations
CPU Time Limit
Each request has a 10,000ms (10 second) CPU time limit. Excessive logging can contribute to this limit.
Impact of logging:
console.log()is fast but not free- Logging large objects or long strings is more expensive
- Hundreds of log calls per request can impact CPU time
Best Practice: Log important events, not every variable assignment.
Batching
The tail worker batches logs before POSTing to LogStream DO:
- Batch size: Up to 100 log entries
- Batch interval: 1 second maximum
- Purpose: Reduce number of DO requests
Limitations
| Limit | Value | Notes |
|---|---|---|
| Log retention | 0 | No retention, real-time only |
| Maximum message size | 10 KB | Per log entry |
| Maximum concurrent viewers | 10 | Per server |
| WebSocket connection timeout | 5 minutes | Idle connections are closed |
Exceeding limits:
- Messages over 10 KB are truncated
- 11th viewer cannot connect until slot opens
- Idle connections (no activity for 5 min) are closed
Security
Authentication
Log access requires:
- Valid session cookie (signed JWT)
- User owns the server (developer relationship)
- Server is in active status
Unauthorized access: Returns 401 or 403 error.
Isolation
Logs are isolated per server slug. You cannot access logs from servers you don't own.
Encryption
WebSocket connections use TLS (WSS protocol).
Comparison with CloudFlare Logs
| Feature | mctx Logs | Cloudflare Logs |
|---|---|---|
| Real-time streaming | ✅ Yes | ✅ Yes (via wrangler tail) |
| Retention | ❌ No | ❌ No (Logpush for retention) |
| Dashboard integration | ✅ Yes | ❌ No |
| Authentication | ✅ Automatic | ⚠️ Requires CF token |
| Filtering | ❌ No | ✅ Yes |
mctx logs are built for quick debugging in the dashboard. For advanced logging needs, consider Cloudflare Logpush or external services.
External Logging Integration
For persistent logs, send from your server code to external services:
Example: HTTP Logging Endpoint
async function logToExternal(message: string, level: string, env: Env) {
await fetch("https://logs.example.com/ingest", {
method: "POST",
headers: {
Authorization: `Bearer ${env.LOGGING_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
timestamp: Date.now(),
level,
message,
server: "my-mcp-server",
}),
});
}
// Use in your handler
console.log(`[INFO] Processing request`); // Still shows in mctx
await logToExternal("Processing request", "info", env); // Persisted externallyRecommended Services
- Datadog - Full observability platform
- New Relic - Application performance monitoring
- Logtail - Simple log aggregation
- Axiom - High-volume log analytics
- Better Stack - Developer-focused logging
See Also
- Server Requirements - Adding logging to your code
- Environment Variables - Configuration management
See something wrong? Report it or suggest an improvement — your feedback helps make these docs better.
Versioning
How versioned endpoints work on mctx. URL format, semantic versioning, version lifecycle, routing, and multiple concurrent versions.
Slug Reservation & Cooling-Off Period
How mctx reserves subdomains after server deletion. 7-day cooling-off period on production, DNS hijacking protection, and test environment behavior.