Expose a read-only tool with MCP
Start a local MCP server, discover its tool and resource, and check how it handles an unsupported argument.
Give the host one operation
Model Context Protocol (MCP) gives an application a way to discover tools and read resources from a server. A tool has a name, input schema, and result. A resource has an identifier and content the application can read. This example exposes branch hours as a tool and a visiting guide as a text resource.
Use a Python 3 virtual environment. Save both files in the same folder and install the SDK version used by the lab. This first step works without Ollama.
View library_mcp.py
from typing import Literal
from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations
server = FastMCP("Fictional library")
GUIDE_URI = "library://guide/visiting"
HOURS = {"central": "09:00–17:00", "west": "10:00–16:00"}
@server.resource(GUIDE_URI, mime_type="text/plain")
def visiting_guide() -> str:
return "Ask at the front desk for a visitor card. Equipment bookings go to the media desk."
@server.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False))
def lookup_hours(branch: Literal["central", "west"]) -> dict[str, str]:
"""Return the opening hours for one fictional library branch."""
return {"branch": branch, "hours": HOURS[branch]}
if __name__ == "__main__":
server.run(transport="stdio")View inspect_library_mcp.py
import asyncio
from datetime import timedelta
from pathlib import Path
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from library_mcp import GUIDE_URI
def library_server():
return StdioServerParameters(
command=sys.executable,
args=[str(Path(__file__).with_name("library_mcp.py").resolve())],
)
async def main():
async with stdio_client(library_server()) as (read, write):
async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=15)) as session:
await session.initialize()
tools = await session.list_tools()
resources = await session.list_resources()
print("Tools:", ", ".join(tool.name for tool in tools.tools))
print("Resources:", ", ".join(str(item.uri) for item in resources.resources))
guide = await session.read_resource(GUIDE_URI)
print("Visiting guide:", guide.contents[0].text)
result = await session.call_tool("lookup_hours", {"branch": "central"})
if result.isError or result.structuredContent is None:
raise ValueError("The server could not return the branch hours.")
print("Central hours:", result.structuredContent["hours"])
invalid = await session.call_tool("lookup_hours", {"branch": "north"})
if not invalid.isError:
raise ValueError("Expected the server to reject an unknown branch.")
print("Unknown branch rejected.")
if __name__ == "__main__":
asyncio.run(main())python -m pip install "mcp==1.30.0"
python inspect_library_mcp.py
Check the tool and resource
The client launches the server as a child process, initializes the connection, and exchanges protocol messages over standard input and output. Use standard error for any server logs you add. The tool accepts only central or west. The visiting guide can be read using library://guide/visiting.
The client lists lookup_hours, reads the visiting guide, prints 09:00–17:00 for central, and reports that the unknown branch was rejected.
Try changing the inputs
Change the valid call in the inspector to west. Compare the printed hours with the dictionary in the server. The resource should still contain the same visiting guide. The server runs a local script you can read, so inspect the code before adding another capability.
Check your understanding
Does marking a tool read-only in its MCP annotations enforce what the function can do?
Annotations describe intended behavior. The implementation and the permissions of the process determine what it can do.
Sources
Progress is saved in this browser.