Source

mcp/ModelContextProtocol.ts

import { FastMCP, Tool } from "fastmcp";
import { Logger, Logging } from "@decaf-ts/logging";
import { FastMCPSessionAuth } from "./types";

/**
 * @description Validates and normalizes a semantic version string.
 * @summary Ensures the provided version follows semantic versioning (major.minor.patch) and returns a tuple-like template string typed as `${number}.${number}.${number}`.
 * @template
 * @param {string} version The version string to validate, expected in the form MAJOR.MINOR.PATCH.
 * @return {string} The normalized version string if valid.
 * @function validateVersion
 * @memberOf module:decorator-validation
 */
function validateVersion(version: string): `${number}.${number}.${number}` {
  const regexp = /(\d+)\.(\d+)\.(\d+)/g;
  const match = regexp.exec(version);
  if (!match)
    throw new Error(
      `Invalid version string. should obey semantic versioning: ${version}`
    );
  return `${match[1]}.${match[2]}.${match[3]}` as `${number}.${number}.${number}`;
}

/**
 * @description Fluent builder for creating a configured ModelContextProtocol instance.
 * @summary Collects MCP configuration including a semantic version, a name, and a registry of tools, offering chainable methods and a final build step to produce a ready-to-use ModelContextProtocol.
 * @param {string} [name] The name of the MCP instance to build.
 * @param {string} [version] The semantic version of the MCP instance.
 * @param {Record<string, Tool<any, any>>} [tools] A map of tool configurations indexed by tool name.
 * @class
 * @example
 * // Build a new MCP with a single tool
 * const mcp = ModelContextProtocol.builder
 *   .setName("Example")
 *   .setVersion("1.0.0")
 *   .addTool({ name: "do", description: "", parameters: z.any(), execute: async () => "ok" })
 *   .build();
 * @mermaid
 * sequenceDiagram
 *   participant Dev as Developer
 *   participant B as Builder
 *   participant MCP as ModelContextProtocol
 *   Dev->>B: setName("Example")
 *   B-->>Dev: Builder
 *   Dev->>B: setVersion("1.0.0")
 *   B-->>Dev: Builder
 *   Dev->>B: addTool(tool)
 *   B-->>Dev: Builder
 *   Dev->>B: build()
 *   B->>MCP: new ModelContextProtocol(FastMCP)
 *   B-->>Dev: ModelContextProtocol
 */
class Builder {
  name!: string;
  version!: `${number}.${number}.${number}`;
  tools: Record<string, Tool<any, any>> = {};

  log = Logging.for("MCP Builder");

  constructor() {}

  /**
   * @description Sets the MCP instance name.
   * @summary Assigns a human-readable identifier to the builder configuration and enables method chaining.
   * @param {string} value The name to assign to the MCP instance.
   * @return {this} The current builder instance for chaining.
   */
  setName(value: string) {
    this.name = value;
    this.log.debug(`name set to ${value}`);
    return this;
  }

  /**
   * @description Sets and validates the semantic version for the MCP instance.
   * @summary Parses the provided value against semantic versioning and stores the normalized version; enables method chaining.
   * @param {string} value The semantic version string (e.g., "1.2.3").
   * @return {this} The current builder instance for chaining.
   */
  setVersion(value: string) {
    this.version = validateVersion(value);
    this.log.debug(`version set to ${value}`);
    return this;
  }

  /**
   * @description Registers a new tool in the builder.
   * @summary Adds a tool configuration by its unique name to the internal registry, throwing if a tool with the same name already exists.
   * @template Auth extends FastMCPSessionAuth
   * @param {Tool<Auth, any>} config The tool configuration object, including a unique name and an execute handler.
   * @return {this} The current builder instance for chaining.
   */
  addTool<Auth extends FastMCPSessionAuth = undefined>(
    config: Tool<Auth, any>
  ) {
    const { name } = config;
    if (name in this.tools) throw new Error(`tool ${name} already registered`);
    this.tools[name] = config;
    this.log.debug(`tool ${name} added`);
    return this;
  }

  /**
   * @description Finalizes the configuration and produces a ModelContextProtocol instance.
   * @summary Validates required fields (name and version), constructs a FastMCP instance, registers all configured tools, and returns a ModelContextProtocol wrapping the MCP.
   * @template Auth extends FastMCPSessionAuth
   * @return {ModelContextProtocol<Auth>} A fully initialized ModelContextProtocol instance.
   */
  build<
    Auth extends FastMCPSessionAuth = undefined,
  >(): ModelContextProtocol<Auth> {
    if (!this.name) throw new Error("name is required");
    if (!this.version) throw new Error("version is required");
    const mcp = new FastMCP<Auth>({
      name: this.name,
      version: this.version,
    });
    Object.values(this.tools).forEach((tool) => {
      try {
        mcp.addTool(tool);
      } catch (e: unknown) {
        throw new Error(`Failed to add tool ${tool.name}: ${e}`);
      }
    });
    this.log.info(`${this.name} MCP built`);
    this.log.debug(
      `${this.name} MCP - available tools: ${Object.keys(this.tools).join(", ")}`
    );
    return new ModelContextProtocol(mcp);
  }
}

/**
 * @description A thin wrapper around FastMCP providing a typed interface for model-centric protocols.
 * @summary Encapsulates a configured FastMCP instance and exposes factory utilities via a static Builder for constructing MCPs with tools, versioning, and naming semantics.
 * @template Auth extends FastMCPSessionAuth Authentication payload type stored in the MCP session, or undefined for no auth.
 * @param {FastMCP<Auth>} mcp The underlying FastMCP instance used to register and run tools.
 * @class
 * @example
 * // Using the builder
 * const protocol = ModelContextProtocol.builder
 *   .setName("Validator")
 *   .setVersion("1.2.3")
 *   .addTool({ name: "ping", description: "", parameters: z.any(), execute: async () => "pong" })
 *   .build();
 * @mermaid
 * sequenceDiagram
 *   participant Dev as Developer
 *   participant B as ModelContextProtocol.Builder
 *   participant MCP as FastMCP
 *   participant Proto as ModelContextProtocol
 *   Dev->>B: setName()/setVersion()/addTool()
 *   Dev->>B: build()
 *   B->>MCP: new FastMCP({ name, version })
 *   B->>MCP: addTool(tool...)
 *   B->>Proto: new ModelContextProtocol(MCP)
 *   B-->>Dev: ModelContextProtocol
 */
export class ModelContextProtocol<Auth extends FastMCPSessionAuth = undefined> {
  /**
   * @description Lazily obtains a logger instance for this protocol wrapper.
   * @summary Uses the Logging facility to create a context-aware Logger bound to this instance.
   * @return {Logger} A logger instance for this class.
   */
  protected get log(): Logger {
    return Logging.for(this as any);
  }

  constructor(protected readonly mcp: FastMCP<Auth>) {}

  /**
   * @description Alias to the inner Builder class for external access.
   * @summary Exposes the builder type to consumers to enable typed construction of ModelContextProtocol instances.
   */
  static readonly Builder = Builder;

  /**
   * @description Factory accessor for a new Builder instance.
   * @summary Creates a new builder to fluently configure and construct a ModelContextProtocol.
   * @return {Builder} A new builder instance.
   */
  static get builder() {
    return new ModelContextProtocol.Builder();
  }

  /**
   * @description Validates a semantic version string.
   * @summary Utility wrapper around the module-level validateVersion to keep a typed validator close to the class API.
   * @param {string} version The version string to validate.
   * @return {string} The normalized semantic version string.
   */
  private static validateVersion(
    version: string
  ): `${number}.${number}.${number}` {
    return validateVersion(version);
  }
}