@prefecthq/fastmcp-ts 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -74,7 +74,7 @@ function resolveSensitiveHeaders(options) {
74
74
  for (const h of options?.exposeHeaders ?? []) set.delete(h.toLowerCase());
75
75
  return set;
76
76
  }
77
- function buildHttpRequestContext(req, sensitiveHeaders) {
77
+ function buildHttpRequestContext(req, sensitiveHeaders, logger) {
78
78
  const headers = new Headers();
79
79
  const redacted = [];
80
80
  req.headers.forEach((value, name) => {
@@ -89,7 +89,8 @@ function buildHttpRequestContext(req, sensitiveHeaders) {
89
89
  try {
90
90
  const u = new URL(req.url);
91
91
  url = u.pathname + u.search;
92
- } catch {
92
+ } catch (err) {
93
+ logger?.debug("request URL parse failed; using origin-form as-is", { component: "http", error: err });
93
94
  url = req.url;
94
95
  }
95
96
  return { headers, redactedHeaderNames: redacted, method: req.method, url };
@@ -147,7 +148,7 @@ var SESSION_CLOSE_CALLBACKS_KEY = "__fastmcp_session_close_callbacks";
147
148
  var SESSION_STATE_MODERN_HTTP_ERROR = "[fastmcp] Session state is not available on modern HTTP requests (protocol revision 2026-07-28). Each request runs statelessly with no shared session store. Use ctx.requestState() to read per-request state. Use ctx.mintRequestState() to carry state across a multi-round-trip flow.";
148
149
  var SESSION_STATE_STATELESS_HTTP_ERROR = "[fastmcp] Session state is not available on a stateless HTTP server (RunOptions.stateless or FASTMCP_STATELESS_HTTP). Each request runs against a fresh store that is discarded when the request ends. Use ctx.requestState() to read per-request state. Use ctx.mintRequestState() to carry state across a multi-round-trip flow. Turn stateless off to use session state, or keep state in your own external store.";
149
150
  var SERVER_INITIATED_STATELESS_HTTP_ERROR = "[fastmcp] Server-initiated requests (ctx.elicit, ctx.sample, ctx.listRoots) are not available on a stateless HTTP server (RunOptions.stateless or FASTMCP_STATELESS_HTTP). They need a session: client capabilities are negotiated once at initialize, and the client replies on a separate request. Return inputRequired({ requestState }) to carry a multi-round-trip flow without a session; the embedded-request form of inputRequired needs a session too. Turn stateless off to use server-initiated requests.";
150
- function createContext(server, sdkCtx, auth, sessionState, requestStateCodec, stateless, sensitiveHeaders) {
151
+ function createContext(server, sdkCtx, auth, sessionState, requestStateCodec, stateless, sensitiveHeaders, logger) {
151
152
  const requestId = String(sdkCtx.mcpReq.id);
152
153
  const progressToken = sdkCtx.mcpReq._meta?.progressToken;
153
154
  const isModernEra = sdkCtx.mcpReq.envelope !== void 0;
@@ -165,16 +166,16 @@ function createContext(server, sdkCtx, auth, sessionState, requestStateCodec, st
165
166
  return {
166
167
  auth,
167
168
  requestId,
168
- http: httpReq ? buildHttpRequestContext(httpReq, sensitive) : void 0,
169
+ http: httpReq ? buildHttpRequestContext(httpReq, sensitive, logger) : void 0,
169
170
  log,
170
- debug: (msg, logger) => log("debug", msg, logger),
171
- info: (msg, logger) => log("info", msg, logger),
172
- notice: (msg, logger) => log("notice", msg, logger),
173
- warning: (msg, logger) => log("warning", msg, logger),
174
- error: (msg, logger) => log("error", msg, logger),
175
- critical: (msg, logger) => log("critical", msg, logger),
176
- alert: (msg, logger) => log("alert", msg, logger),
177
- emergency: (msg, logger) => log("emergency", msg, logger),
171
+ debug: (msg, logger2) => log("debug", msg, logger2),
172
+ info: (msg, logger2) => log("info", msg, logger2),
173
+ notice: (msg, logger2) => log("notice", msg, logger2),
174
+ warning: (msg, logger2) => log("warning", msg, logger2),
175
+ error: (msg, logger2) => log("error", msg, logger2),
176
+ critical: (msg, logger2) => log("critical", msg, logger2),
177
+ alert: (msg, logger2) => log("alert", msg, logger2),
178
+ emergency: (msg, logger2) => log("emergency", msg, logger2),
178
179
  async reportProgress(progress, total, message) {
179
180
  if (progressToken === void 0) return;
180
181
  await sdkCtx.mcpReq.notify({
@@ -256,9 +257,16 @@ function createContext(server, sdkCtx, auth, sessionState, requestStateCodec, st
256
257
  requestState: () => sdkCtx.mcpReq.requestState(),
257
258
  async mintRequestState(payload) {
258
259
  if (requestStateCodec) return requestStateCodec.mint(payload, sdkCtx);
259
- console.warn(
260
- "[fastmcp] ctx.mintRequestState() called without FastMCPOptions.requestState configured \u2014 the resulting requestState is unsigned. Configure FastMCPOptions.requestState for any state that influences authorization, resource access, or business logic."
261
- );
260
+ if (logger) {
261
+ logger.warn(
262
+ "ctx.mintRequestState() called without FastMCPOptions.requestState configured; the resulting requestState is unsigned. Configure FastMCPOptions.requestState for any state that influences authorization, resource access, or business logic.",
263
+ { component: "context" }
264
+ );
265
+ } else {
266
+ console.warn(
267
+ "[fastmcp] ctx.mintRequestState() called without FastMCPOptions.requestState configured \u2014 the resulting requestState is unsigned. Configure FastMCPOptions.requestState for any state that influences authorization, resource access, or business logic."
268
+ );
269
+ }
262
270
  return JSON.stringify(payload);
263
271
  },
264
272
  getState: (key) => {
@@ -296,7 +304,7 @@ var CustomRouteRegistry = class {
296
304
  }
297
305
  if (methods.includes("OPTIONS")) {
298
306
  throw new Error(
299
- `customRoute ${path}: OPTIONS cannot be routed \u2014 the CORS preflight handler answers OPTIONS before route dispatch`
307
+ `customRoute ${path}: OPTIONS cannot be routed \u2014 it is reserved for the CORS preflight and never dispatches to routes`
300
308
  );
301
309
  }
302
310
  if (this._routes.has(path)) {
@@ -355,13 +363,14 @@ function resolveHealth(health) {
355
363
  }
356
364
  return { path, status, body };
357
365
  }
358
- async function serveCustomRouteNode(handler, req, res) {
366
+ async function serveCustomRouteNode(handler, req, res, logger) {
359
367
  const { toWebRequest } = await import("@modelcontextprotocol/node");
360
368
  let response;
361
369
  try {
362
370
  response = await handler(await toWebRequest(req));
363
371
  } catch (error) {
364
- console.error("[fastmcp] custom route handler failed:", error);
372
+ if (logger) logger.error("custom route handler failed", { component: "routes", error });
373
+ else console.error("[fastmcp] custom route handler failed:", error);
365
374
  res.writeHead(500, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "Internal Server Error" }));
366
375
  return;
367
376
  }
@@ -369,7 +378,8 @@ async function serveCustomRouteNode(handler, req, res) {
369
378
  if (response.body) {
370
379
  const stream = Readable.fromWeb(response.body);
371
380
  stream.on("error", (error) => {
372
- console.error("[fastmcp] custom route response stream failed:", error);
381
+ if (logger) logger.error("custom route response stream failed", { component: "routes", error });
382
+ else console.error("[fastmcp] custom route response stream failed:", error);
373
383
  res.destroy();
374
384
  });
375
385
  stream.pipe(res);
@@ -428,21 +438,68 @@ function runMiddlewareChain(middleware, method, request, mcpContext, handler) {
428
438
  return dispatch(0);
429
439
  }
430
440
  var LoggingMiddleware = class {
431
- constructor(emit = console.log) {
441
+ constructor(emit) {
432
442
  this.emit = emit;
433
443
  }
434
444
  emit;
445
+ logger;
446
+ /** @internal Called by FastMCP when this middleware is attached. */
447
+ _bindLogger(logger) {
448
+ if (!this.emit) this.logger = logger;
449
+ }
450
+ // Three output paths, each shaped for its audience:
451
+ // - explicit `emit`: byte-identical to the pre-logger-injection format
452
+ // (arrows and the ✗ glyph), since callers may already parse it.
453
+ // - `this.logger` (the LOGGER branch): the spec invariant that injected
454
+ // loggers receive no symbols — plain text plus structured meta.
455
+ // - stderr fallback (detached from a server): presentation output, like
456
+ // DefaultSink, so it keeps the arrow/glyph format too.
457
+ emitRequest(method) {
458
+ if (this.emit) {
459
+ this.emit(`[fastmcp] \u2192 ${method}`);
460
+ return;
461
+ }
462
+ if (this.logger) {
463
+ this.logger.info(`request ${method}`);
464
+ return;
465
+ }
466
+ process.stderr.write(`[fastmcp] \u2192 ${method}
467
+ `);
468
+ }
469
+ emitResponse(method, ms) {
470
+ if (this.emit) {
471
+ this.emit(`[fastmcp] \u2190 ${method} (${ms}ms)`);
472
+ return;
473
+ }
474
+ if (this.logger) {
475
+ this.logger.info(`response ${method}`, { ms });
476
+ return;
477
+ }
478
+ process.stderr.write(`[fastmcp] \u2190 ${method} (${ms}ms)
479
+ `);
480
+ }
481
+ emitFailure(method, ms, err) {
482
+ const message = err instanceof Error ? err.message : String(err);
483
+ if (this.emit) {
484
+ this.emit(`[fastmcp] \u2717 ${method} (${ms}ms): ${message}`);
485
+ return;
486
+ }
487
+ if (this.logger) {
488
+ this.logger.warn(`request failed ${method}`, { ms, error: err });
489
+ return;
490
+ }
491
+ process.stderr.write(`[fastmcp] \u2717 ${method} (${ms}ms): ${message}
492
+ `);
493
+ }
435
494
  async onRequest(ctx, next) {
436
495
  const t0 = Date.now();
437
- this.emit(`[fastmcp] \u2192 ${ctx.method}`);
496
+ this.emitRequest(ctx.method);
438
497
  try {
439
498
  const result = await next();
440
- this.emit(`[fastmcp] \u2190 ${ctx.method} (${Date.now() - t0}ms)`);
499
+ this.emitResponse(ctx.method, Date.now() - t0);
441
500
  return result;
442
501
  } catch (err) {
443
- this.emit(
444
- `[fastmcp] \u2717 ${ctx.method} (${Date.now() - t0}ms): ${err instanceof Error ? err.message : String(err)}`
445
- );
502
+ this.emitFailure(ctx.method, Date.now() - t0, err);
446
503
  throw err;
447
504
  }
448
505
  }
@@ -744,7 +801,7 @@ var ToolResult = class {
744
801
  }
745
802
  result;
746
803
  };
747
- function convertResult(value, stateless) {
804
+ function convertResult(value, stateless, logger) {
748
805
  if (isInputRequiredResult(value)) {
749
806
  assertInputRequestsAllowedStateless(value, stateless);
750
807
  return value;
@@ -781,9 +838,16 @@ function convertResult(value, stateless) {
781
838
  return { content: [] };
782
839
  }
783
840
  if (Array.isArray(value)) {
784
- console.warn(
785
- "[fastmcp] Tool returned an array \u2014 structuredContent will not be set (MCP spec requires a plain object). Wrap in an object or use ToolResult to suppress this warning."
786
- );
841
+ if (logger) {
842
+ logger.warn(
843
+ "Tool returned an array; structuredContent will not be set (MCP spec requires a plain object). Wrap in an object or use ToolResult to suppress this warning.",
844
+ { component: "tool" }
845
+ );
846
+ } else {
847
+ console.warn(
848
+ "[fastmcp] Tool returned an array \u2014 structuredContent will not be set (MCP spec requires a plain object). Wrap in an object or use ToolResult to suppress this warning."
849
+ );
850
+ }
787
851
  return {
788
852
  content: [{ type: "text", text: JSON.stringify(value) }]
789
853
  };
@@ -799,7 +863,7 @@ function convertResult(value, stateless) {
799
863
  content: [{ type: "text", text: String(value) }]
800
864
  };
801
865
  }
802
- async function toJsonSchema(schema, context) {
866
+ async function toJsonSchema(schema, context, logger) {
803
867
  const maybeNative = schema;
804
868
  if (typeof maybeNative.toJsonSchema === "function") {
805
869
  try {
@@ -807,7 +871,8 @@ async function toJsonSchema(schema, context) {
807
871
  if (result !== null && typeof result === "object") {
808
872
  return result;
809
873
  }
810
- } catch {
874
+ } catch (err) {
875
+ logger?.debug("toJsonSchema strategy failed", { component: "tool", strategy: "native", error: err });
811
876
  }
812
877
  }
813
878
  try {
@@ -815,12 +880,13 @@ async function toJsonSchema(schema, context) {
815
880
  return z.toJSONSchema(
816
881
  schema
817
882
  );
818
- } catch {
883
+ } catch (err) {
884
+ logger?.debug("toJsonSchema strategy failed", { component: "tool", strategy: "zod-v4", error: err });
819
885
  }
820
886
  const where = context ? ` for ${context}` : "";
821
- console.warn(
822
- `[fastmcp] Could not auto-generate JSON schema${where}. Sending { type: 'object' } to clients. Provide an explicit 'inputSchema' in ToolConfig to suppress this warning.`
823
- );
887
+ const message = `Could not auto-generate JSON schema${where}. Sending { type: 'object' } to clients. Provide an explicit 'inputSchema' in ToolConfig to suppress this warning.`;
888
+ if (logger) logger.warn(message, { component: "tool" });
889
+ else console.warn(`[fastmcp] ${message}`);
824
890
  return { type: "object" };
825
891
  }
826
892
  async function validateInput(schema, input, throwAsProtocolError = false) {
@@ -986,6 +1052,366 @@ function normalizeCompletion(raw) {
986
1052
  }
987
1053
  var EMPTY_COMPLETION = { values: [] };
988
1054
 
1055
+ // src/server/cors.ts
1056
+ var DEFAULT_CORS_METHODS = ["GET", "POST", "DELETE", "OPTIONS"];
1057
+ var DEFAULT_CORS_ALLOWED_HEADERS = [
1058
+ "Content-Type",
1059
+ "Authorization",
1060
+ "Mcp-Session-Id",
1061
+ "MCP-Protocol-Version",
1062
+ "Mcp-Method",
1063
+ "Mcp-Name"
1064
+ ];
1065
+ var DEFAULT_CORS_EXPOSED_HEADERS = ["Mcp-Session-Id"];
1066
+ function hasInvalidHeaderChar(value) {
1067
+ return /\s/.test(value) || [...value].some((ch) => ch.charCodeAt(0) < 32 || ch.charCodeAt(0) === 127);
1068
+ }
1069
+ function mergedHeaderList(defaults, extra, option) {
1070
+ if (extra !== void 0 && !Array.isArray(extra)) {
1071
+ throw new Error(`Invalid cors.${option}: must be an array of header names, got: ${JSON.stringify(extra)}`);
1072
+ }
1073
+ const merged = [...defaults];
1074
+ const seen = new Set(defaults.map((h) => h.toLowerCase()));
1075
+ for (const header of extra ?? []) {
1076
+ if (typeof header !== "string" || header.trim() === "") {
1077
+ throw new Error(`Invalid cors.${option}: entries must be non-empty strings, got: ${JSON.stringify(header)}`);
1078
+ }
1079
+ if (hasInvalidHeaderChar(header)) {
1080
+ throw new Error(
1081
+ `Invalid cors.${option}: entries must not contain whitespace or control characters, got: ${JSON.stringify(header)}`
1082
+ );
1083
+ }
1084
+ if (!seen.has(header.toLowerCase())) {
1085
+ seen.add(header.toLowerCase());
1086
+ merged.push(header);
1087
+ }
1088
+ }
1089
+ return merged.join(", ");
1090
+ }
1091
+ function resolveOriginMode(origin) {
1092
+ if (origin === void 0 || origin === "*") return { kind: "any" };
1093
+ if (typeof origin === "function") return { kind: "predicate", test: origin };
1094
+ const list = typeof origin === "string" ? [origin] : origin;
1095
+ if (!Array.isArray(list) || list.length === 0) {
1096
+ throw new Error(
1097
+ `Invalid cors.origin: expected '*', an origin, a non-empty array of origins, or a function, got: ${JSON.stringify(origin)}`
1098
+ );
1099
+ }
1100
+ for (const entry of list) {
1101
+ if (typeof entry !== "string" || entry === "") {
1102
+ throw new Error(`Invalid cors.origin: entries must be non-empty strings, got: ${JSON.stringify(entry)}`);
1103
+ }
1104
+ if (entry === "*") {
1105
+ throw new Error(
1106
+ "Invalid cors.origin: '*' inside an origin array never matches (browsers never send it as a literal Origin); use origin: '*' (the string) for wildcard"
1107
+ );
1108
+ }
1109
+ if (entry.endsWith("/")) {
1110
+ throw new Error(
1111
+ `Invalid cors.origin "${entry}": an Origin header never carries a trailing slash, so this entry can never match; drop the trailing slash`
1112
+ );
1113
+ }
1114
+ }
1115
+ return { kind: "list", origins: new Set(list) };
1116
+ }
1117
+ function resolveCors(cors) {
1118
+ if (cors === false) return null;
1119
+ const opts = cors === void 0 || cors === true ? {} : cors;
1120
+ if (typeof opts !== "object" || opts === null || Array.isArray(opts)) {
1121
+ throw new Error(`Invalid cors option: expected boolean or object, got ${typeof cors}`);
1122
+ }
1123
+ const originMode = resolveOriginMode(opts.origin);
1124
+ if (opts.credentials !== void 0 && typeof opts.credentials !== "boolean") {
1125
+ throw new Error(`Invalid cors.credentials: must be a boolean, got: ${JSON.stringify(opts.credentials)}`);
1126
+ }
1127
+ if (opts.credentials === true && originMode.kind === "any") {
1128
+ throw new Error(
1129
+ "Invalid cors config: credentials: true requires an explicit origin \u2014 browsers reject 'Access-Control-Allow-Origin: *' on credentialed requests; set cors.origin to your site origin(s)"
1130
+ );
1131
+ }
1132
+ let methods = DEFAULT_CORS_METHODS;
1133
+ if (opts.methods !== void 0) {
1134
+ if (!Array.isArray(opts.methods) || opts.methods.length === 0) {
1135
+ throw new Error("Invalid cors.methods: must be a non-empty array of method names");
1136
+ }
1137
+ methods = [
1138
+ ...new Set(
1139
+ opts.methods.map((m) => {
1140
+ if (typeof m !== "string" || m.trim() === "") {
1141
+ throw new Error(`Invalid cors.methods: entries must be non-empty strings, got: ${JSON.stringify(m)}`);
1142
+ }
1143
+ if (hasInvalidHeaderChar(m)) {
1144
+ throw new Error(
1145
+ `Invalid cors.methods: entries must not contain whitespace or control characters, got: ${JSON.stringify(m)}`
1146
+ );
1147
+ }
1148
+ return m.toUpperCase();
1149
+ })
1150
+ )
1151
+ ];
1152
+ }
1153
+ const maxAge = opts.maxAge ?? null;
1154
+ if (maxAge !== null && (!Number.isInteger(maxAge) || maxAge < 0)) {
1155
+ throw new Error(`Invalid cors.maxAge: must be a non-negative integer (seconds), got: ${JSON.stringify(opts.maxAge)}`);
1156
+ }
1157
+ return {
1158
+ originMode,
1159
+ methods: methods.join(", "),
1160
+ allowedHeaders: mergedHeaderList(DEFAULT_CORS_ALLOWED_HEADERS, opts.allowedHeaders, "allowedHeaders"),
1161
+ exposedHeaders: mergedHeaderList(DEFAULT_CORS_EXPOSED_HEADERS, opts.exposedHeaders, "exposedHeaders"),
1162
+ credentials: opts.credentials === true,
1163
+ maxAge
1164
+ };
1165
+ }
1166
+ function allowOriginValue(cors, requestOrigin, logger) {
1167
+ switch (cors.originMode.kind) {
1168
+ case "any":
1169
+ return "*";
1170
+ case "list":
1171
+ return requestOrigin !== void 0 && cors.originMode.origins.has(requestOrigin) ? requestOrigin : null;
1172
+ case "predicate": {
1173
+ if (requestOrigin === void 0) return null;
1174
+ try {
1175
+ return cors.originMode.test(requestOrigin) ? requestOrigin : null;
1176
+ } catch (err) {
1177
+ if (logger) logger.error("cors.origin predicate threw; denying origin", { component: "cors", error: err });
1178
+ else console.error("[fastmcp] cors.origin predicate threw; denying origin:", err);
1179
+ return null;
1180
+ }
1181
+ }
1182
+ }
1183
+ }
1184
+ function baseHeaders(cors, requestOrigin, logger) {
1185
+ const headers = {};
1186
+ if (cors.originMode.kind !== "any") headers["Vary"] = "Origin";
1187
+ const origin = allowOriginValue(cors, requestOrigin, logger);
1188
+ if (origin === null) return headers;
1189
+ headers["Access-Control-Allow-Origin"] = origin;
1190
+ if (cors.credentials) headers["Access-Control-Allow-Credentials"] = "true";
1191
+ return headers;
1192
+ }
1193
+ function corsPreflightHeaders(cors, requestOrigin, logger) {
1194
+ const headers = baseHeaders(cors, requestOrigin, logger);
1195
+ if (headers["Access-Control-Allow-Origin"] === void 0) return headers;
1196
+ headers["Access-Control-Allow-Methods"] = cors.methods;
1197
+ headers["Access-Control-Allow-Headers"] = cors.allowedHeaders;
1198
+ if (cors.maxAge !== null) headers["Access-Control-Max-Age"] = String(cors.maxAge);
1199
+ return headers;
1200
+ }
1201
+ function corsResponseHeaders(cors, requestOrigin, logger) {
1202
+ const headers = baseHeaders(cors, requestOrigin, logger);
1203
+ if (headers["Access-Control-Allow-Origin"] === void 0) return headers;
1204
+ headers["Access-Control-Expose-Headers"] = cors.exposedHeaders;
1205
+ return headers;
1206
+ }
1207
+
1208
+ // src/shared/terminal.ts
1209
+ import chalk from "chalk";
1210
+ var theme = {
1211
+ primary: chalk.bold.cyan,
1212
+ success: chalk.green,
1213
+ warning: chalk.yellow,
1214
+ error: chalk.red,
1215
+ muted: chalk.dim.gray,
1216
+ value: chalk.white,
1217
+ label: chalk.bold,
1218
+ url: chalk.cyan.underline,
1219
+ code: chalk.dim.white
1220
+ };
1221
+ var isUnicode = process.env["TERM"] !== "dumb" && !process.env["CI"];
1222
+ var symbols = {
1223
+ success: isUnicode ? "\u2713" : "\u221A",
1224
+ failure: isUnicode ? "\u2717" : "x",
1225
+ info: isUnicode ? "\u25C6" : "*",
1226
+ pointer: isUnicode ? "\u2192" : ">",
1227
+ warning: isUnicode ? "\u26A0" : "!",
1228
+ pending: isUnicode ? "\u25CB" : "-",
1229
+ active: isUnicode ? "\u25CF" : "*",
1230
+ ellipsis: isUnicode ? "\u2026" : "...",
1231
+ reload: isUnicode ? "\u21BA" : "~",
1232
+ separator: isUnicode ? "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" : "----------------------------------"
1233
+ };
1234
+
1235
+ // src/server/logger.ts
1236
+ var FRAMEWORK_LOG_LEVELS = ["debug", "info", "warn", "error", "silent"];
1237
+ var LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40, silent: 50 };
1238
+ function isFrameworkLogLevel(value) {
1239
+ return typeof value === "string" && FRAMEWORK_LOG_LEVELS.includes(value);
1240
+ }
1241
+ function envLogLevel() {
1242
+ const raw = process.env["FASTMCP_LOG_LEVEL"];
1243
+ if (raw === void 0 || raw.trim() === "") return void 0;
1244
+ const normalized = raw.trim().toLowerCase();
1245
+ if (!isFrameworkLogLevel(normalized)) {
1246
+ throw new Error(
1247
+ `[fastmcp] FASTMCP_LOG_LEVEL must be one of debug/info/warn/error/silent (case-insensitive). Received: ${JSON.stringify(raw)}`
1248
+ );
1249
+ }
1250
+ return normalized;
1251
+ }
1252
+ function formatMetaValue(value) {
1253
+ if (value instanceof Error) return value.stack ?? String(value);
1254
+ if (typeof value === "string") return value;
1255
+ try {
1256
+ return JSON.stringify(value);
1257
+ } catch {
1258
+ return String(value);
1259
+ }
1260
+ }
1261
+ function jsonMeta(meta) {
1262
+ const plain = Object.fromEntries(Object.entries(meta).map(([k, v]) => [k, v instanceof Error ? String(v) : v]));
1263
+ try {
1264
+ return JSON.stringify(plain);
1265
+ } catch {
1266
+ return String(plain);
1267
+ }
1268
+ }
1269
+ var DefaultSink = class {
1270
+ mode;
1271
+ constructor(mode) {
1272
+ this.mode = mode ?? (process.stderr.isTTY === true && process.env["NO_COLOR"] === void 0 ? "pretty" : "plain");
1273
+ }
1274
+ write(line) {
1275
+ process.stderr.write(`${line}
1276
+ `);
1277
+ }
1278
+ plainLine(level, message, meta) {
1279
+ const metaStr = meta && Object.keys(meta).length > 0 ? ` ${jsonMeta(meta)}` : "";
1280
+ return `[fastmcp] ${level.toUpperCase()} ${message}${metaStr}`;
1281
+ }
1282
+ prettyMeta(meta) {
1283
+ if (!meta || Object.keys(meta).length === 0) return "";
1284
+ const pairs = Object.entries(meta).map(([k, v]) => `${k}=${formatMetaValue(v)}`).join(" ");
1285
+ return ` ${theme.muted(pairs)}`;
1286
+ }
1287
+ emit(level, message, meta) {
1288
+ if (this.mode === "plain") {
1289
+ this.write(this.plainLine(level, message, meta));
1290
+ return;
1291
+ }
1292
+ switch (level) {
1293
+ case "debug":
1294
+ this.write(theme.muted(`\xB7 ${message}`) + this.prettyMeta(meta));
1295
+ break;
1296
+ case "info":
1297
+ this.write(`${theme.muted(symbols.info)} ${message}${this.prettyMeta(meta)}`);
1298
+ break;
1299
+ case "warn":
1300
+ this.write(`${theme.warning(symbols.warning)} ${theme.warning(message)}${this.prettyMeta(meta)}`);
1301
+ break;
1302
+ case "error":
1303
+ this.write(`${theme.error(symbols.failure)} ${theme.error(message)}${this.prettyMeta(meta)}`);
1304
+ break;
1305
+ }
1306
+ }
1307
+ debug(message, meta) {
1308
+ this.emit("debug", message, meta);
1309
+ }
1310
+ info(message, meta) {
1311
+ this.emit("info", message, meta);
1312
+ }
1313
+ warn(message, meta) {
1314
+ this.emit("warn", message, meta);
1315
+ }
1316
+ error(message, meta) {
1317
+ this.emit("error", message, meta);
1318
+ }
1319
+ /** Startup card (pretty) or single plain info line. Mirrors the CLI's
1320
+ * output.kv/section look (src/cli/ui/output.ts). */
1321
+ banner(fields) {
1322
+ if (this.mode === "plain") {
1323
+ this.info(`starting ${fields.name} v${fields.version} (${fields.transport})`, fields.url !== void 0 ? { url: fields.url } : void 0);
1324
+ return;
1325
+ }
1326
+ const kv = (key, value) => ` ${theme.label(key.padEnd(12))} ${theme.value(value)}`;
1327
+ const rows = [
1328
+ "",
1329
+ ` ${theme.primary("FastMCP")} ${theme.value(fields.name)} ${theme.muted(`v${fields.version}`)}`,
1330
+ ` ${theme.muted(symbols.separator)}`,
1331
+ kv("transport", fields.transport),
1332
+ ...fields.url !== void 0 ? [kv("url", fields.url)] : [],
1333
+ ""
1334
+ ];
1335
+ this.write(rows.join("\n"));
1336
+ }
1337
+ listening(url) {
1338
+ if (this.mode === "plain") {
1339
+ this.info(`listening on ${url}`);
1340
+ return;
1341
+ }
1342
+ this.write(`${theme.success(symbols.success)} listening on ${theme.url(url)}`);
1343
+ }
1344
+ };
1345
+ var ResolvedLogger = class {
1346
+ constructor(sink, level) {
1347
+ this.sink = sink;
1348
+ this.level = level;
1349
+ }
1350
+ sink;
1351
+ level;
1352
+ enabled(level) {
1353
+ return LEVEL_ORDER[level] >= LEVEL_ORDER[this.level];
1354
+ }
1355
+ // meta stays an omitted argument (not `undefined` passed explicitly) when the
1356
+ // caller left it out: a sink like `console` prints a literal "undefined" for a
1357
+ // second argument that is present but empty, and console.info/console.debug
1358
+ // write to stdout, so a stray argument there is a stdio protocol-safety bug too.
1359
+ debug(message, meta) {
1360
+ if (!this.enabled("debug")) return;
1361
+ if (meta === void 0) this.sink.debug(message);
1362
+ else this.sink.debug(message, meta);
1363
+ }
1364
+ info(message, meta) {
1365
+ if (!this.enabled("info")) return;
1366
+ if (meta === void 0) this.sink.info(message);
1367
+ else this.sink.info(message, meta);
1368
+ }
1369
+ warn(message, meta) {
1370
+ if (!this.enabled("warn")) return;
1371
+ if (meta === void 0) this.sink.warn(message);
1372
+ else this.sink.warn(message, meta);
1373
+ }
1374
+ error(message, meta) {
1375
+ if (!this.enabled("error")) return;
1376
+ if (meta === void 0) this.sink.error(message);
1377
+ else this.sink.error(message, meta);
1378
+ }
1379
+ /** Server-start card. Pretty DefaultSink renders a banner block;
1380
+ * every other sink gets one plain info line. Gated as info. */
1381
+ startupBanner(fields) {
1382
+ if (!this.enabled("info")) return;
1383
+ if (this.sink instanceof DefaultSink) {
1384
+ this.sink.banner(fields);
1385
+ return;
1386
+ }
1387
+ this.info(`starting ${fields.name} v${fields.version} (${fields.transport})`, fields.url !== void 0 ? { url: fields.url } : void 0);
1388
+ }
1389
+ /** Listening confirmation. The word "listening" is load-bearing: the CLI's
1390
+ * run command detects a successful start by sniffing stderr for it. */
1391
+ listening(url) {
1392
+ if (!this.enabled("info")) return;
1393
+ if (this.sink instanceof DefaultSink) {
1394
+ this.sink.listening(url);
1395
+ return;
1396
+ }
1397
+ this.info(`listening on ${url}`);
1398
+ }
1399
+ };
1400
+ function resolveLogger(logger, logLevel) {
1401
+ if (logLevel !== void 0 && !isFrameworkLogLevel(logLevel)) {
1402
+ throw new Error(`Invalid logLevel: must be one of debug/info/warn/error/silent, got: ${JSON.stringify(logLevel)}`);
1403
+ }
1404
+ if (logger !== void 0) {
1405
+ for (const method of ["debug", "info", "warn", "error"]) {
1406
+ if (typeof logger[method] !== "function") {
1407
+ throw new Error(`Invalid logger: must implement debug/info/warn/error methods; missing ${method}()`);
1408
+ }
1409
+ }
1410
+ }
1411
+ const level = logLevel ?? envLogLevel() ?? "info";
1412
+ return new ResolvedLogger(logger ?? new DefaultSink(), level);
1413
+ }
1414
+
989
1415
  // src/server/FastMCP.ts
990
1416
  function prefixResourceUri(uri, prefix) {
991
1417
  const idx = uri.indexOf("://");
@@ -1013,7 +1439,7 @@ function toAccessToken(authInfo) {
1013
1439
  };
1014
1440
  }
1015
1441
  var _cliEnvToken;
1016
- async function resolveCliEnvToken(verifier) {
1442
+ async function resolveCliEnvToken(verifier, logger) {
1017
1443
  if (!verifier || !("verify" in verifier)) return void 0;
1018
1444
  if (_cliEnvToken !== void 0) return _cliEnvToken ?? void 0;
1019
1445
  const raw = process.env["FASTMCP_CLI_AUTH_TOKEN"];
@@ -1024,7 +1450,8 @@ async function resolveCliEnvToken(verifier) {
1024
1450
  try {
1025
1451
  _cliEnvToken = await verifier.verify(raw);
1026
1452
  return _cliEnvToken;
1027
- } catch {
1453
+ } catch (err) {
1454
+ logger?.debug("FASTMCP_CLI_AUTH_TOKEN verification failed; continuing unauthenticated", { component: "auth", error: err });
1028
1455
  _cliEnvToken = null;
1029
1456
  return void 0;
1030
1457
  }
@@ -1044,6 +1471,9 @@ var FastMCP = class {
1044
1471
  name;
1045
1472
  version;
1046
1473
  _auth;
1474
+ /** @internal Framework logger; used by same-package modules (proxy, openapi). Not public API. */
1475
+ _logger;
1476
+ _cors;
1047
1477
  _oauth;
1048
1478
  _sensitiveHeaders;
1049
1479
  _toolsPageSize;
@@ -1097,13 +1527,16 @@ var FastMCP = class {
1097
1527
  constructor(options) {
1098
1528
  this.name = options.name;
1099
1529
  this.version = options.version ?? "0.0.1";
1530
+ this._logger = resolveLogger(options.logger, options.logLevel);
1100
1531
  this._auth = options.auth;
1532
+ this._cors = resolveCors(options.http?.cors);
1101
1533
  this._oauth = options.oauth;
1102
1534
  this._sensitiveHeaders = resolveSensitiveHeaders(options.http);
1103
1535
  this._toolsPageSize = options.toolsPageSize ?? 50;
1104
1536
  this._resourcesPageSize = options.resourcesPageSize ?? 50;
1105
1537
  this._promptsPageSize = options.promptsPageSize ?? 50;
1106
1538
  this._middleware = options.middleware ? [...options.middleware] : [];
1539
+ for (const mw of this._middleware) if (mw instanceof LoggingMiddleware) mw._bindLogger(this._logger);
1107
1540
  this._transforms = options.transforms ? [...options.transforms] : [];
1108
1541
  this._cacheHints = options.cacheHints;
1109
1542
  this._requestStateCodec = options.requestState ? createRequestStateCodec(options.requestState) : void 0;
@@ -1167,12 +1600,12 @@ var FastMCP = class {
1167
1600
  return server;
1168
1601
  }
1169
1602
  async _resolveToken(authInfo) {
1170
- return toAccessToken(authInfo) ?? await resolveCliEnvToken(this._auth);
1603
+ return toAccessToken(authInfo) ?? await resolveCliEnvToken(this._auth, this._logger);
1171
1604
  }
1172
1605
  _setupHandlers(server, sessionState, opts) {
1173
1606
  server.setRequestHandler("tools/list", async (req, sdkCtx) => {
1174
1607
  const token = await this._resolveToken(sdkCtx.http?.authInfo);
1175
- const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
1608
+ const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders, this._logger);
1176
1609
  return contextStore.run(
1177
1610
  ctx,
1178
1611
  () => runMiddlewareChain(this._middleware, "tools/list", req.params, ctx, async () => {
@@ -1223,8 +1656,8 @@ var FastMCP = class {
1223
1656
  };
1224
1657
  }
1225
1658
  const t = entry.config;
1226
- const inputSchema = t.inputSchema ?? (t.input ? await toJsonSchema(t.input, `tool "${t.name}" input`) : { type: "object" });
1227
- const outputSchema = t.outputSchema ?? (t.output ? await toJsonSchema(t.output, `tool "${t.name}" output`) : void 0);
1659
+ const inputSchema = t.inputSchema ?? (t.input ? await toJsonSchema(t.input, `tool "${t.name}" input`, this._logger) : { type: "object" });
1660
+ const outputSchema = t.outputSchema ?? (t.output ? await toJsonSchema(t.output, `tool "${t.name}" output`, this._logger) : void 0);
1228
1661
  const uiMeta = clientIsUiCapable && t.ui ? {
1229
1662
  resourceUri: t.ui.resourceUri ?? `ui://${t.name}`,
1230
1663
  ...t.ui.visibility ? { visibility: t.ui.visibility } : {}
@@ -1252,7 +1685,7 @@ var FastMCP = class {
1252
1685
  const synthTool = synthesizedList.find((s) => s.name === requestedName);
1253
1686
  if (synthTool) {
1254
1687
  if (synthTool.auth) await runAuthCheck(synthTool.auth, token);
1255
- const ctx2 = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
1688
+ const ctx2 = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders, this._logger);
1256
1689
  try {
1257
1690
  return await contextStore.run(
1258
1691
  ctx2,
@@ -1273,7 +1706,7 @@ var FastMCP = class {
1273
1706
  () => clearTimeout(timer)
1274
1707
  );
1275
1708
  }
1276
- return convertResult(await executePromise, opts?.stateless);
1709
+ return convertResult(await executePromise, opts?.stateless, this._logger);
1277
1710
  })
1278
1711
  );
1279
1712
  } catch (err) {
@@ -1305,7 +1738,7 @@ var FastMCP = class {
1305
1738
  if (tool.config.auth) await runAuthCheck(tool.config.auth, token);
1306
1739
  const resolvedTool = tool;
1307
1740
  const rawArgs = req.params.arguments ?? {};
1308
- const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
1741
+ const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders, this._logger);
1309
1742
  try {
1310
1743
  return await contextStore.run(
1311
1744
  ctx,
@@ -1327,7 +1760,7 @@ var FastMCP = class {
1327
1760
  }
1328
1761
  let resultValue = await executePromise;
1329
1762
  if (resolvedTool.config.output) resultValue = await validateInput(resolvedTool.config.output, resultValue);
1330
- const callResult = convertResult(resultValue, opts?.stateless);
1763
+ const callResult = convertResult(resultValue, opts?.stateless, this._logger);
1331
1764
  if (resolvedTool.config.ui) {
1332
1765
  const clientIsUi = isUiCapable(server.getClientCapabilities());
1333
1766
  if (!clientIsUi && callResult.structuredContent !== void 0) {
@@ -1347,7 +1780,7 @@ var FastMCP = class {
1347
1780
  });
1348
1781
  server.setRequestHandler("resources/list", async (req, sdkCtx) => {
1349
1782
  const token = await this._resolveToken(sdkCtx.http?.authInfo);
1350
- const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
1783
+ const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders, this._logger);
1351
1784
  return contextStore.run(
1352
1785
  ctx,
1353
1786
  () => runMiddlewareChain(this._middleware, "resources/list", req.params, ctx, async () => {
@@ -1394,7 +1827,7 @@ var FastMCP = class {
1394
1827
  });
1395
1828
  server.setRequestHandler("resources/templates/list", async (req, sdkCtx) => {
1396
1829
  const token = await this._resolveToken(sdkCtx.http?.authInfo);
1397
- const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
1830
+ const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders, this._logger);
1398
1831
  return contextStore.run(
1399
1832
  ctx,
1400
1833
  () => runMiddlewareChain(this._middleware, "resources/templates/list", req.params, ctx, async () => {
@@ -1446,7 +1879,7 @@ var FastMCP = class {
1446
1879
  const resource = resolved.resource;
1447
1880
  const templateParams = resolved.templateParams;
1448
1881
  if (resource.config.auth) await runAuthCheck(resource.config.auth, token);
1449
- const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
1882
+ const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders, this._logger);
1450
1883
  return contextStore.run(
1451
1884
  ctx,
1452
1885
  () => runMiddlewareChain(this._middleware, "resources/read", req.params, ctx, async () => {
@@ -1480,7 +1913,7 @@ var FastMCP = class {
1480
1913
  }
1481
1914
  const resource = resolved.resource;
1482
1915
  if (resource.config.auth) await runAuthCheck(resource.config.auth, token);
1483
- const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
1916
+ const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders, this._logger);
1484
1917
  return contextStore.run(
1485
1918
  ctx,
1486
1919
  () => runMiddlewareChain(this._middleware, "resources/subscribe", req.params, ctx, async () => {
@@ -1498,7 +1931,7 @@ var FastMCP = class {
1498
1931
  if (opts?.stateless) throw new ProtocolError3(ProtocolErrorCode3.MethodNotFound, STATELESS_SUBSCRIBE_ERROR);
1499
1932
  const uri = req.params.uri;
1500
1933
  const token = await this._resolveToken(sdkCtx.http?.authInfo);
1501
- const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
1934
+ const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders, this._logger);
1502
1935
  return contextStore.run(
1503
1936
  ctx,
1504
1937
  () => runMiddlewareChain(this._middleware, "resources/unsubscribe", req.params, ctx, async () => {
@@ -1510,7 +1943,7 @@ var FastMCP = class {
1510
1943
  });
1511
1944
  server.setRequestHandler("prompts/list", async (req, sdkCtx) => {
1512
1945
  const token = await this._resolveToken(sdkCtx.http?.authInfo);
1513
- const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
1946
+ const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders, this._logger);
1514
1947
  return contextStore.run(
1515
1948
  ctx,
1516
1949
  () => runMiddlewareChain(this._middleware, "prompts/list", req.params, ctx, async () => {
@@ -1590,7 +2023,7 @@ var FastMCP = class {
1590
2023
  );
1591
2024
  }
1592
2025
  }
1593
- const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
2026
+ const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders, this._logger);
1594
2027
  return contextStore.run(
1595
2028
  ctx,
1596
2029
  () => runMiddlewareChain(this._middleware, "prompts/get", req.params, ctx, async () => {
@@ -1614,7 +2047,7 @@ var FastMCP = class {
1614
2047
  });
1615
2048
  server.setRequestHandler("completion/complete", async (req, sdkCtx) => {
1616
2049
  const token = await this._resolveToken(sdkCtx.http?.authInfo);
1617
- const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
2050
+ const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders, this._logger);
1618
2051
  return contextStore.run(
1619
2052
  ctx,
1620
2053
  () => runMiddlewareChain(this._middleware, "completion/complete", req.params, ctx, async () => {
@@ -1928,7 +2361,7 @@ var FastMCP = class {
1928
2361
  }
1929
2362
  let resultValue = await executePromise;
1930
2363
  if (tool.config.output) resultValue = await validateInput(tool.config.output, resultValue);
1931
- return convertResult(resultValue);
2364
+ return convertResult(resultValue, void 0, this._logger);
1932
2365
  })
1933
2366
  );
1934
2367
  } catch (err) {
@@ -2205,6 +2638,7 @@ var FastMCP = class {
2205
2638
  */
2206
2639
  use(mw) {
2207
2640
  this._middleware.push(mw);
2641
+ if (mw instanceof LoggingMiddleware) mw._bindLogger(this._logger);
2208
2642
  mw.setup?.(this._primaryServer);
2209
2643
  return this;
2210
2644
  }
@@ -2280,6 +2714,7 @@ var FastMCP = class {
2280
2714
  this._stateless = options?.stateless ?? envBool("FASTMCP_STATELESS_HTTP") ?? false;
2281
2715
  const resolvedHealth = resolveHealth(options?.health);
2282
2716
  if (transport === "stdio") {
2717
+ this._logger.startupBanner({ name: this.name, version: this.version, transport: "stdio" });
2283
2718
  const { StdioServerTransport, serveStdio } = await import("@modelcontextprotocol/server/stdio");
2284
2719
  const stdioTransport = new StdioServerTransport(options?.stdin, options?.stdout);
2285
2720
  this._stdioHandle = serveStdio(
@@ -2345,7 +2780,7 @@ var FastMCP = class {
2345
2780
  if (!this._statelessLegacyHandler) {
2346
2781
  this._statelessLegacyHandler = legacyStatelessFallback(
2347
2782
  () => this._makeServer(/* @__PURE__ */ new Map(), { stateless: true }),
2348
- (error) => console.error("[fastmcp] stateless legacy serving failed:", error)
2783
+ (error) => this._logger.error("stateless legacy serving failed", { component: "http", error })
2349
2784
  );
2350
2785
  }
2351
2786
  return this._statelessLegacyHandler;
@@ -2404,6 +2839,7 @@ var FastMCP = class {
2404
2839
  retryInterval: LEGACY_SSE_RETRY_MS,
2405
2840
  onsessioninitialized: (id) => {
2406
2841
  this._sessions.set(id, { transport: mcpTransport, server: sessionServer, state: sessionState });
2842
+ this._logger.debug("session opened", { component: "http", sessionId: id });
2407
2843
  },
2408
2844
  onsessionclosed: (id) => {
2409
2845
  const session = this._sessions.get(id);
@@ -2412,6 +2848,7 @@ var FastMCP = class {
2412
2848
  for (const cb of callbacks) cb();
2413
2849
  }
2414
2850
  this._sessions.delete(id);
2851
+ this._logger.debug("session closed", { component: "http", sessionId: id });
2415
2852
  }
2416
2853
  });
2417
2854
  await sessionServer.connect(mcpTransport);
@@ -2427,8 +2864,9 @@ var FastMCP = class {
2427
2864
  const opt = this._dnsRebindingOptions;
2428
2865
  if (opt === void 0 && !isLoopbackHost(host) && !_dnsRebindingWarned) {
2429
2866
  _dnsRebindingWarned = true;
2430
- console.warn(
2431
- "[fastmcp] This HTTP server accepts requests from any Host or Origin. Set dnsRebinding in FastMCPOptions to protect local deployments against DNS rebinding."
2867
+ this._logger.warn(
2868
+ "This HTTP server accepts requests from any Host or Origin. Set dnsRebinding in FastMCPOptions to protect local deployments against DNS rebinding.",
2869
+ { component: "http" }
2432
2870
  );
2433
2871
  }
2434
2872
  const hasExplicitAllowlist = opt?.allowedHosts !== void 0 || opt?.allowedOrigins !== void 0;
@@ -2448,11 +2886,24 @@ var FastMCP = class {
2448
2886
  const { requireBearerAuth } = await import("@modelcontextprotocol/server-legacy/auth");
2449
2887
  const oauth = this._oauth;
2450
2888
  const app = express();
2889
+ if (this._cors) {
2890
+ const cors = this._cors;
2891
+ app.use((req, res, next) => {
2892
+ if (req.method === "OPTIONS") {
2893
+ res.writeHead(204, corsPreflightHeaders(cors, req.headers.origin, this._logger)).end();
2894
+ return;
2895
+ }
2896
+ if (req.path === path) {
2897
+ for (const [k, v] of Object.entries(corsResponseHeaders(cors, req.headers.origin, this._logger))) res.setHeader(k, v);
2898
+ }
2899
+ next();
2900
+ });
2901
+ }
2451
2902
  app.use((req, res, next) => {
2452
2903
  const match = this._customRoutes.match(req.path, req.method ?? "");
2453
2904
  if (!match) return next();
2454
2905
  if (match.kind === "method-mismatch") return writeMethodNotAllowed(res, match.allow);
2455
- void serveCustomRouteNode(match.handler, req, res);
2906
+ void serveCustomRouteNode(match.handler, req, res, this._logger);
2456
2907
  });
2457
2908
  const httpServer = await new Promise((resolve, reject) => {
2458
2909
  const srv = app.listen(port, host, () => resolve(srv));
@@ -2476,39 +2927,34 @@ var FastMCP = class {
2476
2927
  );
2477
2928
  this._httpServer = httpServer;
2478
2929
  this._address = { host: bound.address, port: bound.port, path };
2930
+ const url = `http://${bound.address}:${bound.port}${path}`;
2931
+ this._logger.startupBanner({ name: this.name, version: this.version, transport: "http", url });
2932
+ this._logger.listening(url);
2479
2933
  }
2480
2934
  async _runHttpSimple(port, host, path) {
2481
2935
  this._resolveDnsRebindingGuards(host);
2482
2936
  const { createServer } = await import("http");
2483
2937
  const auth = this._auth;
2484
- const corsHeaders = {
2485
- "Access-Control-Allow-Origin": "*",
2486
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
2487
- // Mcp-Session-Id: legacy (2025-era) session routing, still served alongside
2488
- // modern traffic. MCP-Protocol-Version/Mcp-Method/Mcp-Name: required standard
2489
- // headers for 2026-07-28 requests (SEP-2243). Mcp-Param-* (tool-argument
2490
- // mirroring) is deliberately not listed: browser clients skip that mirroring
2491
- // entirely (dynamically named headers cannot be statically allow-listed for
2492
- // credentialed CORS), so no browser ever needs to send it.
2493
- "Access-Control-Allow-Headers": "Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name"
2494
- };
2938
+ const cors = this._cors;
2495
2939
  const httpServer = createServer(async (req, res) => {
2496
- if (req.method === "OPTIONS") {
2497
- res.writeHead(204, corsHeaders).end();
2940
+ if (req.method === "OPTIONS" && cors) {
2941
+ res.writeHead(204, corsPreflightHeaders(cors, req.headers.origin, this._logger)).end();
2498
2942
  return;
2499
2943
  }
2500
2944
  const pathname = req.url?.split("?")[0] ?? "";
2501
2945
  const routeMatch = this._customRoutes.match(pathname, req.method ?? "");
2502
2946
  if (routeMatch) {
2503
2947
  if (routeMatch.kind === "method-mismatch") writeMethodNotAllowed(res, routeMatch.allow);
2504
- else await serveCustomRouteNode(routeMatch.handler, req, res);
2948
+ else await serveCustomRouteNode(routeMatch.handler, req, res, this._logger);
2505
2949
  return;
2506
2950
  }
2507
2951
  if (pathname !== path) {
2508
2952
  res.writeHead(404).end();
2509
2953
  return;
2510
2954
  }
2511
- for (const [k, v] of Object.entries(corsHeaders)) res.setHeader(k, v);
2955
+ if (cors) {
2956
+ for (const [k, v] of Object.entries(corsResponseHeaders(cors, req.headers.origin, this._logger))) res.setHeader(k, v);
2957
+ }
2512
2958
  if (auth) {
2513
2959
  if (isRequestVerifier(auth)) {
2514
2960
  let accessToken;
@@ -2523,8 +2969,9 @@ var FastMCP = class {
2523
2969
  return;
2524
2970
  }
2525
2971
  if (!accessToken.token) {
2526
- console.error(
2527
- "[fastmcp] RequestVerifier.verifyRequest must set AccessToken.token to a stable, non-empty per-identity value. It keys response-cache partitioning and downstream identity."
2972
+ this._logger.error(
2973
+ "RequestVerifier.verifyRequest must set AccessToken.token to a stable, non-empty per-identity value. It keys response-cache partitioning and downstream identity.",
2974
+ { component: "auth" }
2528
2975
  );
2529
2976
  res.writeHead(500, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "verifyRequest returned an AccessToken with an empty token" }));
2530
2977
  return;
@@ -2572,6 +3019,9 @@ var FastMCP = class {
2572
3019
  });
2573
3020
  const bound = httpServer.address();
2574
3021
  this._address = { host: bound.address, port: bound.port, path };
3022
+ const url = `http://${bound.address}:${bound.port}${path}`;
3023
+ this._logger.startupBanner({ name: this.name, version: this.version, transport: "http", url });
3024
+ this._logger.listening(url);
2575
3025
  }
2576
3026
  async close() {
2577
3027
  await Promise.all(this._proxyCloseCallbacks.map((cb) => cb().catch(() => {
@@ -2916,12 +3366,26 @@ function oauthProxy(options) {
2916
3366
  // src/server/proxy.ts
2917
3367
  import { Client } from "@modelcontextprotocol/client";
2918
3368
  import { parseTemplate } from "url-template";
3369
+ function logProbeFailure(logger, what, err) {
3370
+ const code = err?.code;
3371
+ if (code === -32601) {
3372
+ logger.debug(`proxied server does not support ${what}`, { component: "proxy" });
3373
+ } else {
3374
+ logger.warn(`proxied server ${what} probe failed; serving none`, { component: "proxy", error: err });
3375
+ }
3376
+ }
2919
3377
  async function buildProxyFromClient(client, options) {
2920
3378
  const cacheTtl = options?.cacheTtl ?? 3e4;
2921
3379
  const serverInfo = client.getServerVersion();
2922
3380
  const proxyName = options?.name ?? serverInfo?.name ?? "proxy";
2923
3381
  const proxyVersion = serverInfo?.version ?? "0.0.1";
2924
- const proxy = new FastMCP({ name: proxyName, version: proxyVersion });
3382
+ const proxy = new FastMCP({
3383
+ name: proxyName,
3384
+ version: proxyVersion,
3385
+ ...options?.logger !== void 0 ? { logger: options.logger } : {},
3386
+ ...options?.logLevel !== void 0 ? { logLevel: options.logLevel } : {}
3387
+ });
3388
+ const logger = proxy._logger;
2925
3389
  const proxiedTools = /* @__PURE__ */ new Set();
2926
3390
  const proxiedResources = /* @__PURE__ */ new Set();
2927
3391
  const proxiedPrompts = /* @__PURE__ */ new Set();
@@ -2982,7 +3446,8 @@ async function buildProxyFromClient(client, options) {
2982
3446
  proxiedResources.add(resource.uri);
2983
3447
  }
2984
3448
  }
2985
- } catch {
3449
+ } catch (err) {
3450
+ logProbeFailure(logger, "resources", err);
2986
3451
  }
2987
3452
  try {
2988
3453
  const { resourceTemplates } = await client.listResourceTemplates();
@@ -3009,7 +3474,8 @@ async function buildProxyFromClient(client, options) {
3009
3474
  proxiedResources.add(uriTemplate);
3010
3475
  }
3011
3476
  }
3012
- } catch {
3477
+ } catch (err) {
3478
+ logProbeFailure(logger, "resource templates", err);
3013
3479
  }
3014
3480
  for (const uri of proxiedResources) {
3015
3481
  if (!incoming.has(uri)) {
@@ -3116,7 +3582,12 @@ async function createProxy(config, name) {
3116
3582
  { capabilities: {} }
3117
3583
  );
3118
3584
  await client.connect(transport);
3119
- return buildProxyFromClient(client, { cacheTtl: config.cacheTtl, name });
3585
+ return buildProxyFromClient(client, {
3586
+ cacheTtl: config.cacheTtl,
3587
+ name,
3588
+ logger: config.logger,
3589
+ logLevel: config.logLevel
3590
+ });
3120
3591
  }
3121
3592
 
3122
3593
  // src/server/openapi/internal.ts
@@ -3143,8 +3614,9 @@ function deepEqual(a, b) {
3143
3614
  }
3144
3615
  return false;
3145
3616
  }
3146
- function warn(message) {
3147
- console.warn(`[fastmcp] ${message}`);
3617
+ function warn(message, logger) {
3618
+ if (logger) logger.warn(message, { component: "openapi" });
3619
+ else console.warn(`[fastmcp] ${message}`);
3148
3620
  }
3149
3621
 
3150
3622
  // src/server/openapi/parser.ts
@@ -4088,7 +4560,7 @@ var STYLE_DELIMITERS = {
4088
4560
  spaceDelimited: " ",
4089
4561
  pipeDelimited: "|"
4090
4562
  };
4091
- function unflattenArguments(route, flatArgs) {
4563
+ function unflattenArguments(route, flatArgs, logger) {
4092
4564
  const pathParams = {};
4093
4565
  const queryParams = {};
4094
4566
  const headerParams = {};
@@ -4106,7 +4578,7 @@ function unflattenArguments(route, flatArgs) {
4106
4578
  if (value === null || value === void 0) continue;
4107
4579
  const mapping = route.parameterMap[argName];
4108
4580
  if (mapping === void 0) {
4109
- warn(`Argument '${argName}' not found in parameter map for ${route.operationId ?? route.path}`);
4581
+ warn(`Argument '${argName}' not found in parameter map for ${route.operationId ?? route.path}`, logger);
4110
4582
  continue;
4111
4583
  }
4112
4584
  buckets[mapping.location][mapping.openapiName] = value;
@@ -4203,10 +4675,11 @@ function buildUrl(pathTemplate, pathParams, baseUrl) {
4203
4675
  const base = `${baseUrl.replace(/\/+$/, "")}/`;
4204
4676
  return new URL(urlPath.replace(/^\/+/, ""), base).toString();
4205
4677
  }
4206
- function buildRequest(route, flatArgs, baseUrl) {
4678
+ function buildRequest(route, flatArgs, baseUrl, logger) {
4207
4679
  const { pathParams, queryParams, headerParams, cookieParams, body } = unflattenArguments(
4208
4680
  route,
4209
- flatArgs
4681
+ flatArgs,
4682
+ logger
4210
4683
  );
4211
4684
  const serializedQuery = serializeQueryParams(route, queryParams);
4212
4685
  const url = new URL(buildUrl(route.path, pathParams, baseUrl));
@@ -4320,11 +4793,11 @@ function resolveClient(options, spec) {
4320
4793
  function isTimeoutError(error) {
4321
4794
  return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
4322
4795
  }
4323
- async function sendRequest(client, route, args) {
4796
+ async function sendRequest(client, route, args, logger) {
4324
4797
  let url;
4325
4798
  let init;
4326
4799
  try {
4327
- const directed = buildRequest(route, args, client.baseUrl);
4800
+ const directed = buildRequest(route, args, client.baseUrl, logger);
4328
4801
  const authHeaders = client.auth ? await client.auth.getHeaders() : {};
4329
4802
  const headers = {};
4330
4803
  for (const source of [client.headers, authHeaders, directed.headers]) {
@@ -4363,13 +4836,14 @@ async function sendRequest(client, route, args) {
4363
4836
  }
4364
4837
  return response;
4365
4838
  }
4366
- async function runToolRequest(client, route, args, outputSchema) {
4367
- const response = await sendRequest(client, route, args);
4839
+ async function runToolRequest(client, route, args, outputSchema, logger) {
4840
+ const response = await sendRequest(client, route, args, logger);
4368
4841
  const text = await response.text();
4369
4842
  let result;
4370
4843
  try {
4371
4844
  result = JSON.parse(text);
4372
4845
  } catch {
4846
+ logger?.debug("openapi response was not JSON; returning raw text", { component: "openapi" });
4373
4847
  return new ToolResult({ content: [{ type: "text", text }] });
4374
4848
  }
4375
4849
  let structured;
@@ -4388,8 +4862,8 @@ async function runToolRequest(client, route, args, outputSchema) {
4388
4862
  structuredContent: structured
4389
4863
  });
4390
4864
  }
4391
- async function readResourceRequest(client, route, args, uri, mimeType) {
4392
- const response = await sendRequest(client, route, args);
4865
+ async function readResourceRequest(client, route, args, uri, mimeType, logger) {
4866
+ const response = await sendRequest(client, route, args, logger);
4393
4867
  const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
4394
4868
  if (contentType.includes("application/json")) {
4395
4869
  const result = JSON.parse(await response.text());
@@ -4484,7 +4958,8 @@ function createOpenAPIServer(options) {
4484
4958
  }
4485
4959
  } catch (error) {
4486
4960
  warn(
4487
- `Error in routeMapFn for ${route.method} ${route.path}: ${error.message}. Using mapped type.`
4961
+ `Error in routeMapFn for ${route.method} ${route.path}: ${error.message}. Using mapped type.`,
4962
+ server._logger
4488
4963
  );
4489
4964
  }
4490
4965
  }
@@ -4502,13 +4977,14 @@ function createOpenAPIServer(options) {
4502
4977
  }
4503
4978
  return server;
4504
4979
  }
4505
- function applyComponentFn(componentFn, route, component) {
4980
+ function applyComponentFn(componentFn, route, component, logger) {
4506
4981
  if (componentFn === void 0) return;
4507
4982
  try {
4508
4983
  componentFn(route, component);
4509
4984
  } catch (error) {
4510
4985
  warn(
4511
- `Error in componentFn for ${component.kind} '${component.name}': ${error.message}`
4986
+ `Error in componentFn for ${component.kind} '${component.name}': ${error.message}`,
4987
+ logger
4512
4988
  );
4513
4989
  }
4514
4990
  }
@@ -4533,7 +5009,7 @@ function createTool(server, client, route, name, tags, registry, validateOutput,
4533
5009
  ...outputSchema !== null ? { outputSchema } : {},
4534
5010
  tags
4535
5011
  };
4536
- applyComponentFn(componentFn, route, component);
5012
+ applyComponentFn(componentFn, route, component, server._logger);
4537
5013
  server.tool(
4538
5014
  {
4539
5015
  name: component.name,
@@ -4542,7 +5018,7 @@ function createTool(server, client, route, name, tags, registry, validateOutput,
4542
5018
  ...component.outputSchema !== void 0 ? { outputSchema: component.outputSchema } : {},
4543
5019
  tags: component.tags
4544
5020
  },
4545
- async (args) => runToolRequest(client, route, isPlainObject(args) ? args : {}, component.outputSchema)
5021
+ async (args) => runToolRequest(client, route, isPlainObject(args) ? args : {}, component.outputSchema, server._logger)
4546
5022
  );
4547
5023
  }
4548
5024
  function createResource(server, client, route, name, tags, registry, componentFn) {
@@ -4555,7 +5031,7 @@ function createResource(server, client, route, name, tags, registry, componentFn
4555
5031
  mimeType: extractMimeTypeFromRoute(route),
4556
5032
  tags
4557
5033
  };
4558
- applyComponentFn(componentFn, route, component);
5034
+ applyComponentFn(componentFn, route, component, server._logger);
4559
5035
  server.resource(
4560
5036
  {
4561
5037
  uri: component.uri,
@@ -4564,7 +5040,7 @@ function createResource(server, client, route, name, tags, registry, componentFn
4564
5040
  mimeType: component.mimeType,
4565
5041
  tags: component.tags
4566
5042
  },
4567
- async () => readResourceRequest(client, route, {}, component.uri, component.mimeType)
5043
+ async () => readResourceRequest(client, route, {}, component.uri, component.mimeType, server._logger)
4568
5044
  );
4569
5045
  }
4570
5046
  function createTemplate(server, client, route, name, tags, registry, componentFn) {
@@ -4596,7 +5072,7 @@ function createTemplate(server, client, route, name, tags, registry, componentFn
4596
5072
  parameters: parametersSchema,
4597
5073
  tags
4598
5074
  };
4599
- applyComponentFn(componentFn, route, component);
5075
+ applyComponentFn(componentFn, route, component, server._logger);
4600
5076
  server.resource(
4601
5077
  {
4602
5078
  uri: component.uriTemplate,
@@ -4613,7 +5089,8 @@ function createTemplate(server, client, route, name, tags, registry, componentFn
4613
5089
  route,
4614
5090
  templateArguments(route, matched),
4615
5091
  concreteUri,
4616
- component.mimeType
5092
+ component.mimeType,
5093
+ server._logger
4617
5094
  );
4618
5095
  }
4619
5096
  );
@@ -5081,7 +5558,7 @@ var FormInput = class {
5081
5558
  this.server.tool(
5082
5559
  { name, description, ui: { visibility: ["model", "app"] } },
5083
5560
  async () => {
5084
- const jsonSchema = await toJsonSchema(schema, `form "${name}"`);
5561
+ const jsonSchema = await toJsonSchema(schema, `form "${name}"`, this.server._logger);
5085
5562
  const properties = jsonSchema.properties ?? {};
5086
5563
  const required = jsonSchema.required ?? [];
5087
5564
  const fields = Object.entries(properties).map(