python-sdk icon indicating copy to clipboard operation
python-sdk copied to clipboard

Python SDK incorrectly validates Resource URIs as strict URLs, breaking relative paths

Open maxisbey opened this issue 1 month ago • 2 comments

Problem

The Python SDK is stricter than the MCP specification when validating resource URIs. It rejects relative paths like 'users/me' that are valid according to the spec and accepted by the TypeScript SDK.

Error Example

resource = types.Resource(name="test", uri="users/me")
# ValidationError: Input should be a valid URL, relative URL without a base

Root Cause

The Python SDK uses Pydantic's AnyUrl type for URI fields:

Python SDK (types.py:434):

uri: Annotated[AnyUrl, UrlConstraints(host_required=False)]

TypeScript SDK (types.ts:495):

uri: z.string(),  // Plain string, no validation

What the MCP Spec Says

According to the official MCP specification, the uri field is defined as:

  • Type: string (plain string)
  • No JSON Schema format validation (no "format": "uri" constraint)

The spec intentionally allows any string value to support:

  • Relative paths: 'users/me'
  • Custom schemes: 'custom://resource'
  • File paths: 'file:///path'
  • HTTP URLs: 'https://example.com'
  • Application-specific identifiers

Impact

This breaks interoperability when Python clients receive resources from servers that use relative URIs or when Python servers want to return relative URIs. Affects:

  • Toolbox migration from TypeScript to Python (reported by @celine)
  • Any server using relative resource identifiers

Locations to Fix

The AnyUrl type needs to be changed to str in 6 locations in src/mcp/types.py:

  1. Line 434: Resource.uri
  2. Line 505: ReadResourceRequestParams.uri
  3. Line 523: ResourceContents.uri
  4. Line 573: SubscribeRequestParams.uri
  5. Line 594: UnsubscribeRequestParams.uri
  6. Line 612: ResourceUpdatedNotificationParams.uri

Test Case

# Should accept all of these per spec:
Resource(name="test", uri="users/me")  # Relative path
Resource(name="test", uri="file:///path")  # File URL
Resource(name="test", uri="custom://id")  # Custom scheme

maxisbey avatar Nov 04 '25 14:11 maxisbey

@maxisbey I will work on it!

jasiecky avatar Nov 07 '25 15:11 jasiecky

There's also a case to validate empty Url strings as None. This came up w/ OAuthClientMetadata.client_uri (of type AnyHttpUrl | None), which some users were setting as empty.

We could potentially switch AnyHttpUrl with a more permissive type, e.g.:

AnyHttpUrlOrEmpty = Annotated[
   AnyHttpUrl | None, BeforeValidator(lambda v: None if v == "" else v)
]

ochafik avatar Nov 25 '25 21:11 ochafik