Module 4: Resources — context and data
Resource URIs and the `reservo://` scheme
Description
Every resource in this guide starts the same way: reservo://policies/.... This lesson explains where that prefix comes from, what each part means, and why designing your own URI scheme is a legitimate —and even recommended— decision when building an MCP server, instead of reusing http:// or file:// out of convenience.
Connection to the module
Lesson 03 already showed you resources/list returning these URIs as if they were obvious. This lesson opens that box: what a URI is, why MCP doesn't impose a fixed scheme, and how to design your own with meaning. Lesson 05 (resources/read) is going to use exactly these URIs as the only argument it needs to read a document — understanding their structure now makes that lesson pure mechanics, with no surprises.
What a URI is (and why MCP chose it)
URI stands for Uniform Resource Identifier — an identifier with standard syntax (defined in RFC 3986, the same standard behind http://, mailto:, ftp://) for uniquely naming something. The general structure is:
scheme://authority/path
| | |
reservo policies /cancellation-policy
MCP chose URIs to identify resources for a simple reason: it's an already-solved standard, with clear syntax rules, that any client already knows how to parse (every modern language ships a URI library in its stdlib — in Python, urllib.parse). It didn't have to invent its own identifier format, just as it didn't have to invent JSON-RPC for the message format (Module 2).
The important part: MCP's specification doesn't require any specific scheme. http://, file://, git://, or a completely invented scheme like reservo:// are all equally valid — the server decides which scheme to use, and the client doesn't need to "recognize" the scheme in advance to be able to read it: it just needs to send the complete URI, exactly as it received it from resources/list, back in resources/read.
Anatomy of reservo://policies/cancellation-policy
Let's parse Reservo's URIs with Python's standard library, and compare against other well-known schemes:
# parse_uris.py
from urllib.parse import urlparse
uris = [
"reservo://policies/cancellation-policy",
"reservo://policies/membership-tiers",
"https://example.com/docs/page",
"file:///project/README.md",
]
for uri in uris:
parsed = urlparse(uri)
print(uri)
print(f" scheme = {parsed.scheme!r}")
print(f" netloc = {parsed.netloc!r}")
print(f" path = {parsed.path!r}")
print()
What to expect:
reservo://policies/cancellation-policy
scheme = 'reservo'
netloc = 'policies'
path = '/cancellation-policy'
reservo://policies/membership-tiers
scheme = 'reservo'
netloc = 'policies'
path = '/membership-tiers'
https://example.com/docs/page
scheme = 'https'
netloc = 'example.com'
path = '/docs/page'
file:///project/README.md
scheme = 'file'
netloc = ''
path = '/project/README.md'
Notice the exact parallel: in https://example.com/docs/page, example.com is the netloc (the "authority," typically a domain) and /docs/page is the path. In reservo://policies/cancellation-policy, Reservo uses policies as the netloc —a category instead of a domain— and /cancellation-policy as the path —the exact identifier within that category—. urllib.parse.urlparse doesn't need to know anything special about reservo:// to parse it correctly: URI syntax is generic, and any new scheme inherits the same parsing for free.
Compare it also with file:///project/README.md: notice the three slashes (file:// + /project/...) — that happens because file:// doesn't use netloc (there's no "authority" for a local file), so netloc stays empty and path starts immediately with /. reservo:// does use netloc (policies), so it doesn't need that third slash.
Designing your own scheme: the three segments that matter
Reservo's scheme was designed with this shape:
reservo://<category>/<identifier>
| | |
server's thematic exact
name group document
- The scheme (
reservo) identifies which server the resource comes from — useful when a client talks to several MCP servers at once (Module 6) and needs to tell at a glance whether a URI belongs to Reservo or to another connected server. - The category (
policies) groups related resources. If Reservo grew and added, say, resources about room availability, you could usereservo://rooms/...as a second category, without colliding withreservo://policies/.... - The identifier (
cancellation-policy,membership-tiers) is the exact name of the document within that category — short, readable, inkebab-case(the same style you already use for tool names in Reservo).
This convention (own scheme + category + identifier) isn't a requirement of the specification —MCP only requires the URI to be valid per RFC 3986—, but it's a solid practice: it lets anyone reading resources/list's catalog guess the rest's structure without needing to read the server's full documentation.
Why not reuse http:// or file://
You could, technically, expose Reservo's policies with URIs like https://reservo.internal/policies/cancellation or file:///data/policies/cancellation.md. The reason not to is the confusion generated by a scheme that already has an established meaning in the ecosystem:
- A client that sees
https://...could reasonably assume it can make a real HTTP request to that URL — and in Reservo's case, there's no HTTP server listening there at all. It would be a syntactically valid URI, but semantically misleading. - A client that sees
file://...could assume a real file exists at that path on the local filesystem — and in Reservo, the policy's text lives as a string in the server process's memory (CANCELLATION_POLICY_TEXTin lesson 03's code), not as a file on disk.
An own scheme (reservo://) makes no false promise: it tells anyone looking at it "this is specific to Reservo, and the only correct way to read it is through the MCP protocol with resources/read — don't treat it as a real URL or as a file path."
Production note: how the official SDK registers a scheme
In the mcp SDK (PyPI; pip install "mcp<2" for the legacy line this guide teaches), declaring a resource with your own scheme is as simple as decorating a function with the exact URI, something like:
# Conceptual code from the official SDK -- NOT installed or run in this guide.
@mcp.resource("reservo://policies/cancellation-policy")
def cancellation_policy() -> str:
return CANCELLATION_POLICY_TEXT
The SDK infers mimeType from the function's return type (a str maps to text/plain by default, unless you declare it explicitly) and automatically assembles the corresponding entry in resources/list — the same work the RESOURCES dictionary and the handle_resources_list function did by hand in lesson 03. The scheme (reservo://) is still your decision, not the SDK's: the library never validates or restricts which scheme you choose, exactly as you saw in this lesson's Exercise 3 — it only saves you from writing the if method == "resources/list" by hand.
Common mistakes
-
Thinking a resource URI's
netlochas to be a real domain. No. Inreservo://policies/cancellation-policy,policiesdoesn't resolve to any network address — it's just a grouping segment the server defined.urlparseplaces it innetlocbecause syntactically it occupies that position (after//, before the next/), not because it has to behave like a DNS domain. -
Modifying a URI before passing it to
resources/read. The URI you got fromresources/listhas to be passed exactly, byte for byte, toresources/read— not trimmed, not with changed casing, not with an extra or missing/. It's an exact identifier, not text you can "normalize" on your own. -
Assuming every MCP server uses the same scheme. Each server chooses its own — you're going to see completely different schemes across third-party servers (some do legitimately use
file://, to expose real files on the system; others invent their own, like Reservo). Module 6 (several servers connected at once) is going to show how a client handles resources from different schemes without confusing them. -
Confusing a resource URI's scheme with MCP's
protocolVersion. They're completely independent concepts — one identifies a document (reservo://...), the other versions the message protocol ("2025-06-18"). They have no relationship to each other whatsoever.
Exercises
Exercise 1: Parse three URIs (Easy)
Using urllib.parse.urlparse, what are the scheme, netloc, and path of these three URIs? Work it out mentally first, then confirm by running the code.
A) reservo://policies/support-hours
B) github://issues/1423
C) postgres://prod-db/orders
See solution
from urllib.parse import urlparse
for uri in ["reservo://policies/support-hours", "github://issues/1423", "postgres://prod-db/orders"]:
p = urlparse(uri)
print(uri, "->", p.scheme, "|", p.netloc, "|", p.path)
reservo://policies/support-hours -> reservo | policies | /support-hours
github://issues/1423 -> github | issues | /1423
postgres://prod-db/orders -> postgres | prod-db | /orders
- A)
scheme='reservo',netloc='policies',path='/support-hours'. - B)
scheme='github',netloc='issues',path='/1423'— a hypothetical scheme a GitHub MCP server might use to expose issues as resources. - C)
scheme='postgres',netloc='prod-db',path='/orders'— again hypothetical: an MCP server over a database could expose a table's schema as a resource with a scheme like this.
Exercise 2: Design a new server's scheme (Medium)
You're designing an MCP server for an internal UI component library. It needs to expose, as resources: the color style guide (a fixed document), and the specs for three components (Button, Modal, Table, each a separate fixed document). Design the complete URIs for all four, following this lesson's scheme://category/identifier pattern, and justify your choice of scheme and categories.
See solution
uikit://guides/color-style-guide
uikit://components/button
uikit://components/modal
uikit://components/table
Justification: uikit as the scheme identifies that these resources belong to this specific server (just as reservo identifies Reservo) — short, no spaces, recognizable. Two distinct categories because they're two conceptually different types of content: guides for general documentation (the style guide, which isn't specific to any component) and components for individual specs of each UI piece — that way, if an accessibility guide gets added tomorrow, it would go under uikit://guides/accessibility without mixing in with the component specs. Each component's identifier uses the same name as the component in the code (button, modal, table, lowercase by URI convention), so it's trivial to guess a new component's URI without having to check resources/list first.
Exercise 3: Why doesn't the specification validate the scheme? (Hard)
A teammate proposes that MCP should require a closed list of valid schemes (for example, only allowing file://, https://, and git://), to keep every server from "inventing" its own. Argue, from MCP's design as an open protocol (Module 1: the M×N problem), why that restriction would be counterproductive.
See solution
Restricting valid schemes would reintroduce, at another level, the same M×N problem MCP exists to solve. If the specification required a closed list of schemes, every server would have to force its data to fit one of those preexisting schemes even when it doesn't fit well —for example, a database server would have to pretend its tables are "files" under file://, or a support ticketing server would have to pretend its tickets are URLs under https://—, producing exactly the semantic ambiguity this lesson's "Why not reuse http:// or file://" section explains needs to be avoided.
MCP's real design solves this differently: it doesn't restrict the set of schemes, it standardizes how any scheme, whatever it is, gets discovered and read. resources/list always returns the same metadata format (uri/name/description/mimeType) no matter what scheme the URI has, and resources/read always receives a complete URI and returns contents in the same format (uri/mimeType/text or blob) no matter the scheme. A generic MCP client never needs to "recognize" reservo:// specifically to be able to read it — it just needs to speak the protocol. That's, in miniature, the same M+N design decision that motivates the whole guide: standardizing the exchange mechanism, not each server's specific content.
Summary and next step
- A resource URI follows the standard
scheme://authority/pathsyntax, parseable withurllib.parse.urlparsewithout needing special code per scheme. - MCP does not require a fixed scheme — each server designs its own. Reservo uses
reservo://<category>/<identifier>, run and parsed in this lesson. - Reusing
http://orfile://without there really being an HTTP server or a real file is misleading; an own scheme avoids that false promise. - The URI
resources/listreturns is passed exactly toresources/read— not modified, not normalized, not trimmed.
Next lesson: 05 — resources/read. With the URI scheme already understood, this is the method that uses that URI to bring back the document's complete content — run reading Reservo's two policies, and the -32002 error triggered with a nonexistent URI.
Additional resources
- Model Context Protocol — Specification 2025-06-18: Resources — Confirms the specification doesn't restrict a resource URI's scheme.
- RFC 3986 — Uniform Resource Identifier (URI): Generic Syntax — The standard behind the
scheme://authority/pathsyntax any URI uses, includingreservo://. - Python —
urllib.parse—urlparseand the rest of the standard utilities for breaking down and building URIs.