Resources
- Lesson 5
- 50 min
- Module 2: Design useful server capabilities
Lesson 5 of 22 · MCP: Connect AI Applications to Tools and DataResources
1. Understand the integration problem
3. Build and inspect a local server
4. Connect an AI application
5. Remote access and authorization
6. Protect tools and user control
7. Operate and maintain the integration
What you will be able to do
Give every policy document a stable address that never changes, so that an application can list it, read it whole and cite it. Then decide who may read which document, and give one answer for every refusal.
Before you startWhat to know and to have ready
What you need to know
- Tools, contracts and bounds, from Tools.
- Organizations and roles (
owner,staff,read_only), from the authentication course’s Users and organizations.
Your project
~/projects/support-mcpafter the last lesson, with the virtual environment active:python -m pytest -qprints20 passed.- This lesson adds no file. It reads the resource part of
support_mcp/contracts.py.
Shelf marks in a library
Section titled “Shelf marks in a library”Think of a library. Every book has a shelf mark, such as HIS 942.1 ROW. The mark tells you where the book is, and it stays the same for years. When a new edition arrives, the book keeps its mark, and only the edition on the first page changes.
You can find a book in two ways. You can read the catalogue, a list of every mark. Or you can work out a mark from the pattern: section, number, author. Both ways lead to the same shelf.
You also choose a book yourself, and you read all of it. The librarian does not choose for you, and does not hand you one sentence from page 40.
Some books stay in a staff room. The librarian checks your card before handing one over. If you may not see it, the answer is “we do not have that”.
The problem: a snippet is not the policy
Section titled “The problem: a snippet is not the policy”Grace owns Larkfield’s policies. She asks Amira a simple question: “When the assistant answers about a damaged item, what does the model read?”
Amira shows her. The search tool returns a snippet of the right policy, as the contract allows. Here is that snippet, from the course’s recording of search_knowledge for “return a damaged item”:
… no, you can keep or recycle the damaged item. - Items of 50 dollars or more: we send a free return label, or arrange a …The full policy, damaged-or-wrong-items, has 867 characters and six sections. The first section is “Report within 14 days”. The snippet starts in the middle of the third. A reply written from it can tell a customer to keep a cheap item, and forget the 14 days and the photo. A bounded search result is right for finding a policy. It is wrong for quoting one.
Tomás suggests another tool: get_policy(name). Kwame asks two questions. First, who decides when to read the whole policy? With a tool, only the model decides. Sam cannot open the policy himself, or attach it to the conversation. Second, what is the policy’s name? “The third search result” changes when the ranking changes. A reply that cites a policy needs an address that stays.
The idea: resources with stable URIs
Section titled “The idea: resources with stable URIs”A resource is content that a server offers for reading, such as a document. Each resource has a URI, a Uniform Resource Identifier: a short string that names one thing, like a web address. The application, or the person, chooses which resources to read. The model does not choose. That is the main difference from a tool.
A client finds resources in two ways, like the library’s catalogue and its pattern:
resources/listreturns the list of concrete resources, each with its URI, a name, a title and a type.resources/templates/listreturns resource templates: patterns with placeholders in braces, such aspolicy://{tenant}/{doc_id}. A template describes a whole family of URIs without listing each one. The pattern follows a standard, URI Template (RFC 6570).
Then resources/read with one URI returns the content, whole.
Amira’s design has one URI per policy document:
policy://larkfield/returns-policyIt has three parts. policy:// is the scheme, a name that Larkfield chooses for its own kind of content. larkfield is the organization. returns-policy is the document’s ID. The ID stays the same when the text changes; the document’s version number changes instead. So a reply that cites policy://larkfield/returns-policy still points to the returns policy next year.
A tiny example: one URI, three checks
Section titled “A tiny example: one URI, three checks”The resource part of contracts.py holds the template, and a regular expression that takes a URI apart:
POLICY_URI_TEMPLATE = "policy://{tenant}/{doc_id}"POLICY_URI = re.compile(r"^policy://(?P<tenant>[a-z]+)/(?P<doc_id>[a-z0-9-]{1,60})$")
def policy_uri(tenant: str, doc_id: str) -> str: return POLICY_URI_TEMPLATE.format(tenant=tenant, doc_id=doc_id)POLICY_URI allows only lowercase letters for the organization, and only lowercase letters, digits and - for the ID, up to 60 characters. Here is a real Python session with it:
>>> from support_mcp import contracts>>> m = contracts.POLICY_URI.match("policy://larkfield/returns-policy")>>> m["tenant"], m["doc_id"]('larkfield', 'returns-policy')>>> contracts.POLICY_URI.match("policy://larkfield/../secrets")>>> contracts.POLICY_URI.match("https://larkfield.example/returns")>>>The first URI matches, and the two parts come out. The other two print nothing: match returns None, so they are not policy URIs. ../secrets tries to climb out of the folder, as in a file path. The https:// address is a web page, not a Larkfield document. Neither can reach the business service.
A correct form is only the first check. Before the server answers resources/read, it makes three:
Text description of the diagram
The URI policy://larkfield/returns-policy has three parts: the scheme policy://, the organization larkfield, and the document ID returns-policy, which stays when the text changes. The template for the family is policy://{tenant}/{doc_id}.
The server checks three things, in order. A “no” at any check gives the same answer, −32602 “Resource not found”.
- Does the URI have the form
policy://{tenant}/{doc_id}? - Is
{tenant}the caller’s own organization? - Does the document exist, and may this caller read it?
| URI | Caller | Answer |
|---|---|---|
policy://larkfield/gift-cards |
Sam (staff, Larkfield) | the Markdown text, 433 characters |
policy://bramble/returns-policy |
Sam | −32602 Resource not found (check 2) |
policy://larkfield/refund-approval-procedure |
Omar (read-only, Larkfield) | not found (check 3: staff only) |
policy://larkfield/../secrets |
anyone | not found (check 1: no match) |
Look at the second row. Bramble Books really has a document called returns-policy. Even so, the answer is the same as for a document that does not exist. If the answer were “forbidden”, Sam would learn that Bramble has a returns policy, and could guess its other documents. One answer for both cases tells a caller nothing about the other organization. The authentication course teaches the same rule in Tenant-safe queries.
The organization in the URI is never a key that opens a door. The server compares it with the caller’s own organization, which it learns from the connection, as for tools.
Larkfield: nine documents, nine URIs
Section titled “Larkfield: nine documents, nine URIs”Larkfield has 9 policy documents. One of them, refund-approval-procedure, is for staff only: it tells agents how to approve manual refunds. So the list depends on the caller. Here is a real session with the business service:
>>> from support_mcp.knowledge import Knowledge>>> k = Knowledge()>>> for d in k.documents("larkfield", staff=True): print(contracts.policy_uri("larkfield", d["doc_id"]), d["access"], d["version"])...policy://larkfield/damaged-or-wrong-items public 2policy://larkfield/delivery-policy public 4policy://larkfield/gift-cards public 1policy://larkfield/order-changes-and-cancellations public 2policy://larkfield/refund-approval-procedure staff 2policy://larkfield/refunds-and-payments public 3policy://larkfield/returns-policy public 3policy://larkfield/supplier-aquaflow-hose-reels public 1policy://larkfield/warranty-policy public 5>>> len(k.documents("larkfield", staff=True)), len(k.documents("larkfield", staff=False))(9, 8)>>> k.document("larkfield", "refund-approval-procedure", staff=False)support_mcp.knowledge.NotFound: Not found.Sam, who is staff, gets 9 URIs. Omar, who is read-only, gets 8. When a read-only caller asks for the staff document by its URI, the business service answers “Not found.” The version numbers show the stable IDs at work: the delivery policy is in its fourth version, and its URI has never changed.
In Module 3, the server sends this list as the answer to resources/list. The course’s recording of the finished server, for Sam, is 1,806 bytes with 9 entries. Here is one of them:
{"description": "policy, version 3", "mimeType": "text/markdown", "name": "returns-policy", "title": "Returns policy", "uri": "policy://larkfield/returns-policy"}mimeType says what kind of text comes back: Markdown. The answer to resources/templates/list is 392 bytes. It holds one template, policy://{tenant}/{doc_id}, named policy: “The full text of one policy document of your organization.” And here are the two recorded answers to resources/read, shortened:
{"contents": [{"mimeType": "text/markdown", "text": "# Gift cards\n\n## Buying a gift card\nWe sell gift cards from 25 to 500 dollars. …", "uri": "policy://larkfield/gift-cards"}]}
{"code": -32602, "message": "Resource not found: policy://bramble/returns-policy", "data": {"uri": "policy://bramble/returns-policy"}}The first is a result: the whole gift card policy, 433 characters of Markdown. The second is an error with code −32602, which MCP uses for a resource that is not found. Module 3 explains error codes.
How much text does a resource return? The whole document. Here are the nine Larkfield documents, next to the 300 characters that a search snippet allows:
- one Larkfield document (a resource)
- the search snippet limit
Text description of the chart
A dot plot with two rows, from 0 to 3,000 characters. The first row has nine dots, one per Larkfield document: damaged-or-wrong-items 867, delivery-policy 2,150, gift-cards 433, order-changes-and-cancellations 724, refund-approval-procedure 1,059, refunds-and-payments 1,432, returns-policy 1,744, supplier-aquaflow-hose-reels 873, warranty-policy 2,846. The second row has one dot, the snippet limit, at 300. Every document is longer than a snippet. The shortest, gift cards, has 433 characters; the longest, the warranty policy, has 2,846.
Every document is longer than a snippet. That is the reason for both capabilities: the search finds the right policy, and the resource gives its whole text. The search result even carries the resource’s URI, so the step from one to the other is short.
Who chooses: a tool or a resource
Section titled “Who chooses: a tool or a resource”The same server can offer the same policy in two ways. The difference is who decides to read it. The sequence below is a design for a host, such as the help desk assistant. The course’s own host, in Module 4, uses only tools.
Text description of the diagram
- The host sends
resources/list. The server answers with 9 policy URIs for Sam (8 for a read-only member). - The application chooses a resource. Sam attaches “Returns policy”. The host sends
resources/read policy://larkfield/returns-policy, and gets the whole document in Markdown. - The host sends Sam’s question and the attached text to the model.
- The model chooses a tool. It proposes
search_knowledge. The host sendstools/call, and gets 3 matches: snippets of at most 300 characters, each with itspolicy://URI. - The host gives the matches to the model.
Try the URIs
Section titled “Try the URIs”-
Start Python in the project folder, with the virtual environment active:
Terminal window python -
Type the lines of the two sessions above, from
from support_mcp import contractstok.document(…). Compare each answer with the lesson. Only theTracebacklines beforeNotFounddiffer. -
Now try Bramble Books. Type
len(k.documents("bramble", staff=False)). It prints4: Bramble Books has 5 documents, and one of them,supplier-terms, is for staff only. -
Leave Python with
exit().
This lesson adds no file and no test. The contract test test_policy_uris_are_stable_and_parseable, from the last lesson, already checks the template, the parts and the refused ../secrets.
When to use a resource, and what it costs
Section titled “When to use a resource, and what it costs”Use a resource for content that someone reads whole, and that has a stable identity: a policy, a manual page, a product sheet. It fits when the application or the person should choose it, and when a reply must cite it.
Do not use a resource for everything:
- Customer records, such as tickets. A ticket is personal data, and the assistant needs one at a time, for one task.
get_ticketlets the model ask for the ticket that it works on, with a bounded result. Aresources/listof every ticket would be an unbounded list of customers. - Answers that need a computation, such as a search. The words change every time, so there is nothing stable to name. That is a tool.
- Large or growing content. A resource returns everything. Larkfield’s longest policy is 2,846 characters, which is fine. A 300-page manual should be split into sections, each with its own URI.
Resources also have costs:
- The whole text enters the context. Attach three long policies, and the model reads all of them on every turn.
- The list itself needs checks. The list depends on the caller: 9 documents for Sam, 8 for Omar. So
resources/listmust check the caller, likeresources/read. - Hosts differ. The protocol defines resources, but each host decides how to show them, or whether to use them. Check each host that you plan to support.
Common mistakes
Section titled “Common mistakes”Common mistake
A URI that never matches
Symptom: every read of a document fails, although the document exists. In Python, contracts.POLICY_URI.match("policy://Larkfield/Returns_Policy") prints nothing: the result is None.
Cause: the pattern allows only lowercase letters, digits and -. Larkfield has a capital letter, and Returns_Policy has a capital letter and an underscore.
Fix: build URIs with contracts.policy_uri(tenant, doc_id) from the IDs in the data, never by hand. Do not loosen the pattern: a strict form is the first check.
Common mistake
A staff document that seems to be missing
Symptom: k.document("larkfield", "refund-approval-procedure", staff=False) raises NotFound: Not found., but the document is in data/documents.jsonl.
Cause: this is the access check. The document is marked staff, and the call asked as a read-only member.
Fix: nothing to fix. Use staff=True only for owners and staff. In Module 3, the server takes this value from the caller’s role in the membership table, never from the request.
Reset. This lesson changes no file. If your project is broken, copy the snapshot end-of-m02 as in Where MCP fits, with end-of-m02 instead of start.
Knowledge check
Section titled “Knowledge check”Check your understanding
For each need, choose a tool or a resource, and say who chooses: (a) Sam wants to attach the full warranty policy to a conversation; (b) the model must find which policy talks about double charges; (c) a reply must cite the returns policy so that a reader can open it later.
Show the answer
(a) A resource: policy://larkfield/warranty-policy. Sam, through the application, chooses it, and it comes back whole. (b) A tool: search_knowledge. The model decides during the conversation that it must look something up, and gets bounded matches. (c) A resource URI: policy://larkfield/returns-policy. It stays the same when the policy’s text changes, so the citation still works later. The search result carries this URI, so the model can name it.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Predict the answers of the finished server: (a) Omar (read-only, Larkfield) sends resources/list; (b) Omar reads policy://larkfield/refund-approval-procedure; (c) Sam reads policy://bramble/returns-policy, a document that exists at Bramble Books.
Show the answer
(a) 8 resources: every Larkfield document except the staff-only refund-approval-procedure. (b) −32602 “Resource not found”, at check 3: the document exists, but a read-only member may not read it. (c) −32602 “Resource not found: policy://bramble/returns-policy”, with data.uri, at check 2. That is the same answer as for a document that does not exist, so Sam learns nothing about Bramble Books.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Practical check: in Python, what do these print: contracts.policy_uri('larkfield', 'returns-policy'), contracts.POLICY_URI.match('policy://larkfield/../secrets'), and len(k.documents('larkfield', staff=False))?
Show the answer
'policy://larkfield/returns-policy'; nothing, because the result is None (the form is refused); and 8, because the staff-only procedure is not listed for a read-only caller.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Recap
- A resource is content to read whole, with a URI. The application or the person chooses it; the model does not.
- A stable URI,
policy://larkfield/returns-policy, keeps the document’s ID when its text changes. The version changes instead. - A resource template,
policy://{tenant}/{doc_id}, describes the whole family.resources/listlists the concrete URIs, per caller: 9 for Sam, 8 for Omar. - Before
resources/read, the server checks the form, the organization and the caller’s right to read. Every refusal is the same −32602 “Resource not found”. - Search finds a policy with a 300-character snippet; the resource gives all of it, up to 2,846 characters at Larkfield.
Your project still prints 20 passed. Next, you design the last kind of capability: a task that the person starts.
Next: Prompts