Skip to content

Custom Logger Swallowing Tool Call Logs #163

@RobbieMcKinstry

Description

@RobbieMcKinstry

Hello! Thanks for your work on FastMCP!

I'm having some trouble with the recently released custom logger feature, and I suspect it's a bug.

My customer logger is getting picked up my the framework for its internal logging, but any logs from a tool call are getting swallowed.

Here's my index.ts file:

import { FastMCP } from "@punkpeye/fastmcp";
import { z } from "@zod/zod";
import Logger from "@/logger.ts";

export function run() {
  const logger = new Logger();
  logger.info("Hello from the logger, outside of FastMCP.");
  const server = new FastMCP({
    logger: new Logger(),
    name: "My Server",
    version: "1.0.0",
  });

  server.addTool({
    name: "add",
    description: "Add two numbers",
    parameters: z.object({
      a: z.number(),
      b: z.number(),
    }),
    execute: async (args, { log }) => {
      log.info("This message is getting sent into a blackhole.");
      console.log("Proof the tool call is running.");
      return String(args.a + args.b);
    },
  });

  server.start({
    transportType: "httpStream",
    httpStream: {
      endpoint: "/sse",
      port: 3001,
    },
  });
}

And here is my logging implementation. I'm using logtape, and it's coming out lovely! Aside from this one small issue.

import {
  configure,
  getConsoleSink,
  getLogger,
  Logger as TapeLogger,
} from "@logtape/logtape";
import { getPrettyFormatter } from "@logtape/pretty";
import { Logger } from "@punkpeye/fastmcp";

class PrettyLogger implements Logger {
  readonly logger: TapeLogger;

  constructor() {
    this.logger = getLogger("last.engineer");
  }

  debug(...args: unknown[]): void {
    const msg = args.shift();
    // If we've been given additional arguments, then we print them.
    // Otherwise, we omit them from the format string.
    const logline = args.length ? `${msg} {*}` : `${msg}`;

    this.logger.debug(logline, { ...args });
  }

  info(...args: unknown[]): void {
    console.log(
      "Proof that the info function is not called from the tool (this line will not appear after the tool call)",
    );
    const msg = args.shift();
    // If we've been given additional arguments, then we print them.
    // Otherwise, we omit them from the format string.
    const logline = args.length ? `${msg} {*}` : `${msg}`;
    this.logger.info(logline, { ...args });
  }

  log(...args: unknown[]): void {
    this.info(args);
  }

  warn(...args: unknown[]): void {
    const msg = args.shift();
    // If we've been given additional arguments, then we print them.
    // Otherwise, we omit them from the format string.
    const logline = args.length ? `${msg} {*}` : `${msg}`;
    this.logger.warn(logline, { ...args });
  }

  error(...args: unknown[]): void {
    const msg = args.shift();
    // If we've been given additional arguments, then we print them.
    // Otherwise, we omit them from the format string.
    const logline = args.length ? `${msg} {*}` : `${msg}`;
    this.logger.error(logline, { ...args });
  }
}

await configure({
  sinks: {
    console: getConsoleSink({
      formatter: getPrettyFormatter(),
    }),
  },
  loggers: [
    {
      category: "last.engineer",
      lowestLevel: "info",
      sinks: ["console"],
    },
    // Disable the metalogger. This is not really best for production, but it's not
    // clear how we would want to represent these meta-logs.
    { category: ["logtape", "meta"], sinks: [] },
  ],
});

export default PrettyLogger;

As you'll be able to see, the tool call runs successfully, and the console.logs show that there's nothing wrong with e.g. stdout. It's just that the log argument in the tool call context does not actually call .info. If it did, the console.log I have in there would fire. Since we can see console.log working from the tool call, but not from inside log.info, I have to conclude the issue is with the injected logger.

I can give a full repo to reproduce if you like; I'm running Deno, so it may not be a one-liner for you to reproduce since you'd need to set up the same import mapping as me.

Thanks for your time and assistance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions