Skip to content

@FrontMcp({ providers: [...] }) providers don't propagate to @App tools / resources #403

@alexmercerpo

Description

@alexmercerpo

Describe the bug
Providers registered on the server (top-level @FrontMcp({ providers: [...] })) are not visible to this.get(...) calls inside tools / resources defined in child @Apps. Calls fail with ProviderNotAvailableError: Provider "TemplateRegistry" is not available: not found in local or parent registries even when the @Provider decorator is applied, the class is imported correctly (same module identity), the class is added to @FrontMcp({ providers: [...] }), and the @App that owns the consuming tool is added to @FrontMcp({ apps: [...] }). The framework boots cleanly, server logs show "Scope ready — N apps, N tools", the tool registers (appears in tools/list), and the error fires at tool-execution time when this.get(...) walks local → parent registries and finds nothing. Tools in apps that also declare providers: [TemplateRegistry] on the @App succeed — so the binding works at app scope but the parent chain isn't traversed from server scope.

To Reproduce
Steps to reproduce the behavior:

  1. Define a provider:

    // src/providers/foo.provider.ts
    import { Provider } from '@frontmcp/sdk';
    @Provider({ name: 'Foo' })
    export class Foo { hello() { return 'hi'; } }
  2. Define a tool that consumes it (note no providers on the @App):

    // src/apps/demo/tools/say.tool.ts
    import { Tool, ToolContext, z } from '@frontmcp/sdk';
    import { Foo } from '../../../providers/foo.provider';
    @Tool({
      name: 'say',
      description: 'demo',
      inputSchema: {},
      outputSchema: { msg: z.string() },
    })
    export default class SayTool extends ToolContext {
      async execute() {
        return { msg: this.get(Foo).hello() };  // ← throws ProviderNotAvailableError
      }
    }
    
    // src/apps/demo/demo.app.ts
    import { App } from '@frontmcp/sdk';
    import SayTool from './tools/say.tool';
    @App({ id: 'demo', name: 'Demo', tools: [SayTool] })  // no `providers` field here
    export class DemoApp {}
  3. Register at server scope only:

    // src/main.ts
    import 'reflect-metadata';
    import { FrontMcp } from '@frontmcp/sdk';
    import { DemoApp } from './apps/demo/demo.app';
    import { Foo } from './providers/foo.provider';
    @FrontMcp({
      info: { name: 'demo', version: '0.1.0' },
      apps: [DemoApp],
      providers: [Foo],            // ← server-level registration
    })
    export default class Server {}
  4. Call say — throws ProviderNotAvailableError.

Expected behavior
Per the create-provider skill (.claude/skills/frontmcp-development/references/create-provider.md):

Register at @App level for app-scoped, @FrontMcp for server-scoped
Server-scoped providers are shared; duplicating causes multiple instances

i.e. server-level providers should be visible to every app. The provider-registry's get(token) already recurses into parent registries (if (this.providers) return this.providers.get(token); in @frontmcp/sdk index.js ~line 51710), so the lookup machinery is in place — but the wiring between the server-level registry and the apps' local registries appears not to thread the server providers in as a parent.

Screenshots
N/A — CLI / framework issue, no UI.

Environment (please complete the following information):

  • OS: macOS (Darwin 25.3.0)
  • Node: v24.x
  • Packages: @frontmcp/sdk 1.2.1, @frontmcp/testing 1.2.1, frontmcp 1.2.1

Additional context

Workaround: declare the providers redundantly inside every @App that needs them (providers: [TemplateRegistry, ComponentRegistry, …] on each @App). This works but:

  • Each @App instantiates its own copy of the provider — not strictly "shared singleton" semantics.
  • For pure read-only services (registries over a static set) the duplication is harmless.
  • For mutable shared state (e.g. HistoryService once it persists generations across apps), this silently breaks by giving each app its own history.

If the intent is that @FrontMcp({ providers: [...] }) only applies to the framework's own internal services (and user providers must go on @Apps), update the create-provider skill to say so explicitly — the current text reads as if both registration sites are interchangeable for cross-app sharing. If the intent is that they ARE shared, this is a wiring bug in the SDK's multi-app scope construction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions