Skip to content

Commit 3aa9855

Browse files
Vu-Johnclaude
andcommitted
feat(logging): emit typed structured events from route handlers (wave 1)
Converts chat-ingestion, widget-content, tunnels, and OAuth proxy error paths to emit typed structured events via getRequestLogger when a Hono Context is available, falling back to legacy logger.warn for callers without a request context. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 094c84f commit 3aa9855

5 files changed

Lines changed: 317 additions & 20 deletions

File tree

mcpjam-inspector/server/routes/apps/mcp-apps/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import { Hono } from "hono";
1010
import "../../../types/hono";
1111
import { logger } from "../../../utils/logger";
12+
import { getRequestLogger } from "../../../utils/request-logger";
13+
import { classifyWidgetError } from "../../../utils/error-classify";
1214
import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/app-bridge";
1315
import type {
1416
McpUiResourceCsp,
@@ -92,6 +94,12 @@ apps.post("/widget-content", async (c) => {
9294
const content = contents[0];
9395

9496
if (!content) {
97+
getRequestLogger(c, "routes.apps.mcp-apps").event("widget.resource.failed", {
98+
widgetType: "mcp_apps",
99+
resourceUri: resolvedResourceUri,
100+
cspMode: effectiveCspMode,
101+
errorCode: classifyWidgetError(null, "resource_missing"),
102+
});
95103
return c.json({ error: "No content in resource" }, 404);
96104
}
97105

@@ -116,6 +124,12 @@ apps.post("/widget-content", async (c) => {
116124
} else if ("blob" in content && typeof content.blob === "string") {
117125
html = Buffer.from(content.blob, "base64").toString("utf-8");
118126
} else {
127+
getRequestLogger(c, "routes.apps.mcp-apps").event("widget.resource.failed", {
128+
widgetType: "mcp_apps",
129+
resourceUri: resolvedResourceUri,
130+
cspMode: effectiveCspMode,
131+
errorCode: classifyWidgetError(null, "html_missing"),
132+
});
119133
return c.json({ error: "No HTML content in resource" }, 404);
120134
}
121135

@@ -163,6 +177,12 @@ apps.post("/widget-content", async (c) => {
163177
});
164178

165179
// Return JSON with HTML and metadata for CSP enforcement
180+
getRequestLogger(c, "routes.apps.mcp-apps").event("widget.resource.served", {
181+
widgetType: "mcp_apps",
182+
resourceUri: resolvedResourceUri,
183+
cspMode: effectiveCspMode,
184+
mimeTypeValid,
185+
});
166186
c.header("Cache-Control", "no-cache, no-store, must-revalidate");
167187
return c.json({
168188
html,
@@ -177,6 +197,10 @@ apps.post("/widget-content", async (c) => {
177197
mimeTypeWarning,
178198
});
179199
} catch (error) {
200+
getRequestLogger(c, "routes.apps.mcp-apps").event("widget.resource.failed", {
201+
widgetType: "mcp_apps",
202+
errorCode: classifyWidgetError(error),
203+
});
180204
logger.error("[MCP Apps] Error fetching resource", error);
181205
return c.json(
182206
{ error: error instanceof Error ? error.message : "Unknown error" },

mcpjam-inspector/server/routes/mcp/oauth.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Hono } from "hono";
22
import type { ContentfulStatusCode } from "hono/utils/http-status";
33
import { logger } from "../../utils/logger";
4+
import { getRequestLogger } from "../../utils/request-logger";
5+
import { classifyError } from "../../utils/error-classify";
46
import {
57
executeOAuthProxy,
68
executeDebugOAuthProxy,
@@ -10,6 +12,15 @@ import {
1012

1113
const oauth = new Hono();
1214

15+
function safeHostname(url: string | undefined): string {
16+
if (!url) return "unknown";
17+
try {
18+
return new URL(url).hostname || url;
19+
} catch {
20+
return url;
21+
}
22+
}
23+
1324
/**
1425
* Debug proxy for OAuth flow visualization and testing
1526
* POST /api/mcp/oauth/debug/proxy
@@ -20,17 +31,31 @@ const oauth = new Hono();
2031
* Body: { url: string, method?: string, body?: object, headers?: object }
2132
*/
2233
oauth.post("/debug/proxy", async (c) => {
34+
let proxyUrl: string | undefined;
2335
try {
2436
const { url, method, body, headers } = await c.req.json();
37+
proxyUrl = url;
2538
const result = await executeDebugOAuthProxy({ url, method, body, headers });
2639
return c.json(result);
2740
} catch (error) {
41+
const targetUrlHost = safeHostname(proxyUrl);
2842
if (error instanceof OAuthProxyError) {
43+
getRequestLogger(c, "routes.mcp.oauth").event("mcp.oauth.proxy.failed", {
44+
targetUrlHost,
45+
oauthPhase: "proxy",
46+
errorCode: classifyError(error),
47+
statusCode: error.status,
48+
});
2949
return c.json(
3050
{ error: error.message },
3151
error.status as ContentfulStatusCode,
3252
);
3353
}
54+
getRequestLogger(c, "routes.mcp.oauth").event("mcp.oauth.proxy.failed", {
55+
targetUrlHost,
56+
oauthPhase: "proxy",
57+
errorCode: classifyError(error),
58+
});
3459
logger.error("[OAuth Debug Proxy] Error", error);
3560
return c.json(
3661
{
@@ -50,17 +75,31 @@ oauth.post("/debug/proxy", async (c) => {
5075
* @deprecated Use /debug/proxy for debugging or implement proper OAuth client
5176
*/
5277
oauth.post("/proxy", async (c) => {
78+
let proxyUrl: string | undefined;
5379
try {
5480
const { url, method, body, headers } = await c.req.json();
81+
proxyUrl = url;
5582
const result = await executeOAuthProxy({ url, method, body, headers });
5683
return c.json(result);
5784
} catch (error) {
85+
const targetUrlHost = safeHostname(proxyUrl);
5886
if (error instanceof OAuthProxyError) {
87+
getRequestLogger(c, "routes.mcp.oauth").event("mcp.oauth.proxy.failed", {
88+
targetUrlHost,
89+
oauthPhase: "proxy",
90+
errorCode: classifyError(error),
91+
statusCode: error.status,
92+
});
5993
return c.json(
6094
{ error: error.message },
6195
error.status as ContentfulStatusCode,
6296
);
6397
}
98+
getRequestLogger(c, "routes.mcp.oauth").event("mcp.oauth.proxy.failed", {
99+
targetUrlHost,
100+
oauthPhase: "proxy",
101+
errorCode: classifyError(error),
102+
});
64103
logger.error("OAuth proxy error", error);
65104
return c.json(
66105
{
@@ -77,13 +116,13 @@ oauth.post("/proxy", async (c) => {
77116
* GET /api/mcp/oauth/metadata?url=https://mcp.asana.com/.well-known/oauth-authorization-server/sse
78117
*/
79118
oauth.get("/metadata", async (c) => {
119+
const metadataUrl = c.req.query("url");
80120
try {
81-
const url = c.req.query("url");
82-
if (!url) {
121+
if (!metadataUrl) {
83122
return c.json({ error: "Missing url parameter" }, 400);
84123
}
85124

86-
const result = await fetchOAuthMetadata(url);
125+
const result = await fetchOAuthMetadata(metadataUrl);
87126
if ("status" in result && result.status !== undefined) {
88127
return c.json(
89128
{
@@ -95,12 +134,24 @@ oauth.get("/metadata", async (c) => {
95134

96135
return c.json(result.metadata);
97136
} catch (error) {
137+
const targetUrlHost = safeHostname(metadataUrl);
98138
if (error instanceof OAuthProxyError) {
139+
getRequestLogger(c, "routes.mcp.oauth").event("mcp.oauth.proxy.failed", {
140+
targetUrlHost,
141+
oauthPhase: "metadata",
142+
errorCode: classifyError(error),
143+
statusCode: error.status,
144+
});
99145
return c.json(
100146
{ error: error.message },
101147
error.status as ContentfulStatusCode,
102148
);
103149
}
150+
getRequestLogger(c, "routes.mcp.oauth").event("mcp.oauth.proxy.failed", {
151+
targetUrlHost,
152+
oauthPhase: "metadata",
153+
errorCode: classifyError(error),
154+
});
104155
logger.error("OAuth metadata proxy error", error);
105156
return c.json(
106157
{

mcpjam-inspector/server/routes/mcp/tunnels.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { Hono } from "hono";
2+
import type { Context } from "hono";
23
import { tunnelManager } from "../../services/tunnel-manager";
34
import { LOCAL_SERVER_ADDR } from "../../config";
45
import { cleanupOrphanedTunnels } from "../../services/tunnel-cleanup";
56
import "../../types/hono";
67
import { logger } from "../../utils/logger";
8+
import { getRequestLogger } from "../../utils/request-logger";
9+
import { classifyTunnelError } from "../../utils/error-classify";
710

811
const tunnels = new Hono();
912

@@ -62,6 +65,14 @@ async function fetchNgrokToken(authHeader?: string): Promise<{
6265
};
6366
}
6467

68+
function safeHostname(url: string): string {
69+
try {
70+
return new URL(url).hostname || url;
71+
} catch {
72+
return url;
73+
}
74+
}
75+
6576
// Report tunnel creation to Convex backend
6677
async function recordTunnel(
6778
serverId: string,
@@ -70,6 +81,7 @@ async function recordTunnel(
7081
domainId?: string,
7182
domain?: string,
7283
authHeader?: string,
84+
c?: Context,
7385
): Promise<void> {
7486
const convexUrl = process.env.CONVEX_HTTP_URL;
7587
if (!convexUrl) {
@@ -92,6 +104,14 @@ async function recordTunnel(
92104
body: JSON.stringify({ serverId, url, credentialId, domainId, domain }),
93105
});
94106
} catch (error) {
107+
const tunnelKind = serverId === "shared" ? "shared" : "server";
108+
if (c) {
109+
getRequestLogger(c, "routes.mcp.tunnels").event("tunnel.record_failed", {
110+
tunnelKind,
111+
tunnelDomain: domain,
112+
errorCode: classifyTunnelError(error, "convex_record_failed"),
113+
});
114+
}
95115
logger.error("Failed to record tunnel", error, { serverId, url });
96116
// Don't throw - tunnel is already created, just log the error
97117
}
@@ -167,6 +187,11 @@ tunnels.post("/create", async (c) => {
167187
// Check if tunnel already exists
168188
const existingUrl = tunnelManager.getTunnelUrl();
169189
if (existingUrl) {
190+
getRequestLogger(c, "routes.mcp.tunnels").event("tunnel.created", {
191+
tunnelKind: "shared",
192+
tunnelDomain: safeHostname(existingUrl),
193+
existed: true,
194+
});
170195
return c.json({
171196
url: existingUrl,
172197
existed: true,
@@ -189,13 +214,24 @@ tunnels.post("/create", async (c) => {
189214
domainId,
190215
domain,
191216
authHeader,
217+
c,
192218
);
193219

220+
getRequestLogger(c, "routes.mcp.tunnels").event("tunnel.created", {
221+
tunnelKind: "shared",
222+
tunnelDomain: domain,
223+
existed: false,
224+
credentialIdPresent: !!credentialId,
225+
});
194226
return c.json({
195227
url,
196228
existed: false,
197229
});
198230
} catch (error: any) {
231+
getRequestLogger(c, "routes.mcp.tunnels").event("tunnel.creation_failed", {
232+
tunnelKind: "shared",
233+
errorCode: classifyTunnelError(error),
234+
});
199235
logger.error("Error creating tunnel", error);
200236
return c.json(
201237
{
@@ -214,6 +250,11 @@ tunnels.post("/create/:serverId", async (c) => {
214250
try {
215251
const existingUrl = tunnelManager.getServerTunnelUrl(serverId);
216252
if (existingUrl) {
253+
getRequestLogger(c, "routes.mcp.tunnels").event("tunnel.created", {
254+
tunnelKind: "server",
255+
tunnelDomain: safeHostname(existingUrl),
256+
existed: true,
257+
});
217258
return c.json({
218259
url: existingUrl,
219260
existed: true,
@@ -236,19 +277,30 @@ tunnels.post("/create/:serverId", async (c) => {
236277
domainId,
237278
domain,
238279
authHeader,
280+
c,
239281
);
240282

241283
const serverTunnelUrl = tunnelManager.getServerTunnelUrl(serverId);
242284
if (!serverTunnelUrl) {
243285
throw new Error("Failed to build server tunnel URL");
244286
}
245287

288+
getRequestLogger(c, "routes.mcp.tunnels").event("tunnel.created", {
289+
tunnelKind: "server",
290+
tunnelDomain: domain,
291+
existed: false,
292+
credentialIdPresent: !!credentialId,
293+
});
246294
return c.json({
247295
url: serverTunnelUrl,
248296
serverId,
249297
existed: false,
250298
});
251299
} catch (error: any) {
300+
getRequestLogger(c, "routes.mcp.tunnels").event("tunnel.creation_failed", {
301+
tunnelKind: "server",
302+
errorCode: classifyTunnelError(error),
303+
});
252304
logger.error("Error creating server-specific tunnel", error, { serverId });
253305
return c.json(
254306
{

0 commit comments

Comments
 (0)