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

    P1Significant bug affecting many users, highly requested featurebugSomething isn't workingready for workEnough information for someone to start working on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions