Skip to content

[MCP Bundle] Allow grouping tools into servers by tag, not just by namespace #2426

Description

@wachterjohannes

Follow-up to #2412, which introduced per-server element filtering in the MCP bundle. Raised by @Toflar there, parked so the PR could land.

The use case

Filtering currently takes service ids, FQCNs, namespace prefixes and *:

mcp:
    servers:
        public:
            http: { path: /mcp }
            tools: ['App\Mcp\Public\']

That works when the tools of one server happen to share a namespace. It does not when they are spread across several vendor namespaces, which is the normal case as soon as bundles start shipping tools. @Toflar's proposal is to group by tag instead:

#[McpTool(name: 'current-time', tags: ['foo', 'bar'])]
mcp:
    servers:
        public:
            http: { path: /mcp }
            tools: ['foo']

Beyond namespaces, this would let an application tag a third-party tool into a server from a compiler pass, without touching the vendor code.

The open question

@OskarStark's objection is the thing to solve first: nothing makes a tag unique across vendors. foo from two bundles is the same string, whereas an FQCN or a namespace prefix is unambiguous by construction. A tag design therefore needs a naming convention, or a scoping rule, before it is safe to expose.

Worth deciding:

  • Whether tags are namespaced by convention (acme/public) or scoped to the declaring bundle.
  • What happens on a collision: union, error, or last one wins.
  • Whether a tag can be added from a compiler pass to a tool it does not own, which is the extensibility half of the request.

Why it can wait

The config node is a heterogeneous scalar list, so a tag form can be added additively later, for example as #foo or tag:foo, without changing the meaning of any existing value and without a second BC break. Nothing in #2412 forecloses this.

Activity

  1. Toflar commented on Aug 24, 2026

    @Toflar

    I think maybe we should just allow to define a service that returns the list of tools per server? Then I can implement my own one filtering on whatever I need? Whether that's an array key based on $meta of every McpTool instance or some other custom registry, doesn't matter then.

  2. wachterjohannes commented on Aug 24, 2026

    @wachterjohannes
    MemberAuthor

    @Toflar good idea, and it sidesteps the tag question: no naming convention to invent, no collision rule to define.

    One thing to settle first, so we do not have to redo it: filtering happens at compile time today. McpPass hands each server a service locator with only its own references, which is also what debug:mcp and the typo check live on. A runtime provider would cost us those.

    So I would ask your service at container build, passing the service id, the class and the attribute metadata, and make the current string patterns the default implementation of that same interface instead of a second code path.

    Would that cover your case, or do you actually need to decide at runtime?

  3. Toflar commented on Aug 25, 2026

    @Toflar

    A runtime provider would cost us those.

    To be honest, I think we will need that anyway? As soon as tools become permission-aware as not every user should get the same tools, we have exactly that?

  4. wachterjohannes commented on Aug 25, 2026

    @wachterjohannes
    MemberAuthor

    @Toflar fair point, and yes, we needed exactly that already. we built it at sulu as a plain RegistryInterface decorator: https://github.com/sulu/SuluMcpBundle/blob/1.0/src/Infrastructure/Mcp/FilteredRegistry.php

    so the permission case works today without any bundle change. the registry is per server (mcp.server.<name>.registry), so you can decorate just the one you want.

    that said, i agree it could be more comfortable. our decorator is ~250 lines and almost all of it is proxying RegistryInterface just to filter one list, so a smaller hook for that would be welcome.

    two things we ran into, in case they help here:

    • listing and calling need different rules. getTools() filters by what the current user may see, getTool() deliberately does not, so calling a hidden tool gives a permission denial instead of a fabricated "not found".
    • filtering at registration was not enough on its own. runtime attribute discovery re-adds what we pruned at compile time, so setDiscoveryState() needs the same treatment.

    which is why i would still keep the two apart: compile time decides which tools a server has, and that is what keeps debug:mcp and the typo check meaningful, runtime decides which of those this user sees. the provider idea would cover the first, your permission case is the second and already has a home.

  5. Toflar commented on Aug 25, 2026

    @Toflar

    compile time decides which tools a server has, and that is what keeps debug:mcp and the typo check meaningful, runtime decides which of those this user sees.

    I agree. So tagging a tool to say "this server only has the tools with tag foobar" still seems the way to go for me then. Otherwise platforms like Sulu and Contao etc. cannot group tools from all sorts of namespaces for e.g. the admin backend vs. the frontend etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    FeatureNew featureMCP BundleIssues & PRs about the MCP SDK integration bundle

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions