Skip to content

provideExperimentalWebMcpTools signature prevents type safety with heterogeneous tool inputs #70125

Description

@michael-andre

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

No

Description

The provideExperimentalWebMcpTools function:

  • is defined as generic to leverage TypeScript's type inference in the descriptor object (i.e. type safety and consistency between the inputSchema definition and the execute callback;
  • accepts multiple descriptors as an array.
declare function provideExperimentalWebMcpTools<const InputSchema extends JsonSchemaForInference>(
  tools: ToolDescriptor<InputSchema>[]
): EnvironmentProviders;

This definition unfortunately breaks type inference when using multiple tools with heterogeneous input parameters (that work individually):

provideExperimentalWebMcpTools([
  {
    name: "tool1",
    description: "Tool #1",
    inputSchema: {
      type: "object",
      properties: {
        arg1: { type: "number" }
      },
      required: ["arg1"]
    },
    execute: (args) => {
      const arg1 = args.arg1; // Type inference fails
      return "arg1 number value is " + arg1.toFixed(); // Type ERROR here
    }
  },
  {
    name: "tool2",
    description: "Tool #2",
    inputSchema: {
      type: "object",
      properties: {
        arg2: { type: "string" }
      },
      required: ["arg2"]
    },
    execute: (args) => {
      const arg2 = args.arg2; // Type inference fails
      return "arg2 string value is " + arg2.trim(); // Type ERROR here
    }
  }
])

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw

TypeScript is unable to infer useful types for both `execute` arguments:
- `arg1` is resolved as `number | { [x: string]: unknown; }` instead of `number`
- `arg2` is resolved as `string | { [x: string]: unknown; }` instead of `string`

Please provide the environment you discovered this bug in (run ng version)

angular/core    22.1.1
TypeScript      6.0.3

Anything else?

This is a well known limitation of TypeScript's generics, with no concept of existential types that could relax the overly restrictive constraint of the ToolDescriptor<InputSchema>[] argument (the tools do not need to have homogeneous input types in practice, they are only iterated over in the implementation).

Workarounds

  1. Disable type inference on the method, loose consistency and re-cast arguments: 🥲
// eslint-disable-next-line @typescript-eslint/no-explicit-any
provideExperimentalWebMcpTools<any>([
  {
    inputSchema: { ... }, // Tool #1 with arg1 number param
    execute: (args) => {
      const arg1 = args["arg1"] as number;
      ...
    }
  },
  {
    inputSchema: { ... }, // Tool #2 with arg2 string param
    execute: (args) => {
      const arg2 = args["arg2"] as string;
      ...
    }
  }
])
  1. Give up provideExperimentalWebMcpTools and replace it with its current implementation, with chained calls to declareExperimentalWebMcpTool:
makeEnvironmentProviders([
  provideEnvironmentInitializer(() => {
    void declareExperimentalWebMcpTool({
      inputSchema: { ... }, // Tool #1 with arg1 number param
      execute: (args) => { args.arg1 /* Inference works */ }
    });
    void declareExperimentalWebMcpTool({
      inputSchema: { ... }, // Tool #2 with arg2 string param
      execute: (args) => { args.arg2 /* Inference works */ }
    });
  })
])

Possible solutions

Individual type inference for MCP descriptors AND collection support are hard to mix in the same call.

Based on the previous workaround 2, a non-generic provideExperimentalWebMcpTools method could simply offer a callback to call multiple times for descriptor registration:

export function provideExperimentalWebMcpTools(
  toolsDeclaration: (declare: typeof declareExperimentalWebMcpTool) => Promise<void>
): EnvironmentProviders {
  return makeEnvironmentProviders([
    provideEnvironmentInitializer(() => {
      void toolsDeclaration(declareExperimentalWebMcpTool);
    })
  ]);
}

Usage:

provideExperimentalWebMcpTools(async declare => {
  await declare({ /* Tool 1 */);
  await declare({ /* Tool 2 */);
})

(note that declareExperimentalWebMcpTool promise is unhandled in the current implementation)

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimegemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions