Skip to content

Unordered and partial query parameter matching #149

@disintegrator

Description

@disintegrator

Describe the bug

MCP server does not match resource requests that do no have all query parameters specified in the order they were declared in the ResourceTemplate / UriTemplate. Instead, clients receive a not found error (MCP error -32602).

To Reproduce

Run the following reproducer (e.g. npx tsx repro.ts):

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import {
  McpServer,
  ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";

const server = new McpServer({
  name: "test-server",
  version: "0.0.0",
});

server.resource(
  "example",
  new ResourceTemplate("acme://products{?page,limit}", { list: undefined }),
  async (uri, vars): Promise<ReadResourceResult> => {
    const page = vars.page ?? "1"
    const limit = vars.limit ?? "20"
    return {
      contents: [
        {
          text: `Listing ${vars.limit} products on page ${vars.page}`,
          mimeType: "text/plain",
          uri: "uri",
        },
      ],
    };
  }
);

const client = new Client({
  name: "test-client",
  version: "1.0.0",
});

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
  server.connect(serverTransport),
  client.connect(clientTransport),
]);

const result = await client.readResource({
  uri: "acme://products",
});

console.log(result.contents.map((c) => c.text).join("\n"));

Expected behavior

The script above should print Listing 20 products on page 1 but instead it errors with:

McpError: MCP error -32602: MCP error -32602: Resource acme://products not found

Additional context

I think the current handling of query parameters makes resource templates a little too rigid. My current workaround is to convert certain dynamic resources to tools but I'm not sure that's a good long term solution. I propose changing the behavior of UriTemplate such that the following test cases pass:

import { UriTemplate } from "@modelcontextprotocol/sdk/shared/uriTemplate.js";
import { expect, test } from "vitest";

test("UriTemplate::match treats query parameters as optional", () => {
  const template = new UriTemplate("acme://products{?page,limit}");
  expect(template.match("acme://products")).toEqual({});
  expect(template.match("acme://products?page=2")).toEqual({ page: "2" });
});

test("UriTemplate::match accepts query parameters in arbitrary order", () => {
  const template = new UriTemplate("acme://products{?page,limit,q*}");
  expect(template.match("acme://products?q=cat,dog&limit=40&page=5")).toEqual({
    page: "5",
    limit: "40",
    q: ["cat", "dog"],
  });
});

// npx vitest uritemplate.test.ts

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workingfix proposedBot has a verified fix diff in the commentready for workEnough information for someone to start working on

    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