Connect Botie to Tools with MCP
Several assistants need the same maintenance tools. Share an interface while keeping server identity, permissions and returned content visible.
15 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
A Common Interface for Data Kept in Different Systems
BOTIE
Our invented maintenance service uses that idea to expose manual content. Sharing a protocol does not settle whether a server is trustworthy, which technician may read a record, or whether a proposed action needs approval. Those decisions remain in the host application and the service that owns the data.
Why Write the Manual Connector Again for Every Assistant?
The maintenance team now uses Botie in a web app and in an internal support tool. Both need the same manual lookup, but each integration describes and calls it differently. Maintaining two copies makes it easy for their behaviour to drift.
A shared protocol can describe what a server offers and how a client requests it. Model Context Protocol, or MCP, provides that interface. We will follow a manual lookup across the connection, then see why discovering a tool does not automatically make it safe or permitted to call.
The workshop uses a local registry of labels. It does not open MCP connections or execute remote tools.
Step 1 of 2
Follow one lookup across the boundary
The host manages the model and user interface. Its client connection carries the request to the configured manual server.
- 1
Host application
Chooses permitted capabilities for this request - 2
MCP client connection
Names the server and sends read_manual arguments - 3
Manual server
Validates access and reads the excerpt - 4
Host again
Checks the result and provides it as evidence
Step 2 of 2
Two tools can have the same local name
The configured server is part of the identity. The host allowlist includes only maintenance/read_manual.
| Qualified name | Handler label | Allowed? |
|---|---|---|
| maintenance/read_manual | Read maintained pump excerpts | Yes |
| community/read_manual | Read community notes | No |
maintenance/read_manual
- Handler label
- Read maintained pump excerpts
- Allowed?
- Yes
community/read_manual
- Handler label
- Read community notes
- Allowed?
- No
Shared interfaces still need explicit trust boundaries
The host retains connection identity through discovery, permission and dispatch. A local name alone cannot identify the intended capability.
Which server would receive this lookup?
Keep the server identity attached to the tool name, check the host allowlist, then resolve the registered handler.
BOTIE
Selected handler or refusal reason
Maintained pump excerpts
Follow the operations
1. Requested server and tool
{ "server": "maintenance", "tool": "read_manual" }2. Keep the server with the tool
maintenance/read_manual
3. Permitted server-qualified endpoints
- 0maintenance/read_manual
4. Check the endpoint allowlist
maintenance/read_manual
5. Registered endpoint labels
{ "maintenance/read_manual": "Maintained pump excerpts", "community/read_manual": "Community notes" }6. Resolve the approved handler
Maintained pump excerpts
7. Result
Maintained pump excerpts
The slash names and label registry are a teaching model. Real MCP clients also negotiate capabilities, validate arguments, authenticate and handle transport and tool failures.
1. Distinguish the host, client and server
The host is the application coordinating the user, model and available connections. An MCP client maintains a connection to a server. The server exposes capabilities such as reading maintenance records. A host can connect to several servers, each with a distinct scope and trust relationship.
That separation lets us put the manual integration beside the system that owns the manuals. The host can decide how much returned text enters a model request without moving all data ownership into the assistant.
MCP is an interface between programs. It is not itself an agent loop, a language model or a guarantee that an answer is correct.
2. Choose whether something is a tool, resource or prompt
A tool exposes an operation with named arguments, such as read_manual(asset). Tools can read or write; the word tool does not imply that every invocation changes state. A resource exposes content at an identifier such as manual://P7. A prompt supplies a reusable message template a user can choose.
These roles suggest different interfaces: a tool call, a document the application can include, or a prompt menu. They do not override permission checks. A read operation can reveal private data, and a prompt template can contain instructions the host should inspect before trusting.
The same manual service can offer several of these capabilities because they answer different application needs.
3. Discover a capability before calling it
The client establishes the connection and negotiates the protocol features supported by the server. Listing tools returns descriptions and input schemas. The host can select permitted definitions to make available to a model, then send a requested name and arguments to the correct server.
A schema describes the argument shape. It does not prove that an asset belongs to the technician or that a URL is safe to fetch. The receiving service still validates both data and access. If a server paginates discovery results, the client must follow its cursor rather than assuming one page contains everything.
Keep discovery, selection and execution distinct in the trace. A tool can be listed successfully while a particular call is correctly denied.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Which item is a resource?
Use the information below.
A service offers read_manual(asset), content at manual://P7, and an inspection-question message template.4. Expose the invented manual service in Python
The reference defines a tool, a fixed resource and a prompt. It then uses an in-process client to discover and call the tool without a model. That is enough to test the service interface before adding an agent that chooses calls.
The example follows the current official Python SDK 2.x API. Older tutorials often use FastMCP from mcp.server.fastmcp; do not mix imports from different major versions. Pin your resolved environment once the example works.
The result describes the fixture’s lookup. No model decides what to request in this program.
Run in your own environment
A manual server and a local protocol test
This is a standalone service reference using the official MCP Python SDK. Its test stays inside one Python process.
Use Python 3.11 or later in a separate environment. Install mcp>=2,<3 and anyio, then record resolved versions in a lockfile.
No provider API key is needed for this program. The Client receives the server object for an in-process test.
The fixture has no authentication because it contains only invented public text and is not exposed on a network. Add real access control before connecting private data or deploying a network server.
import anyio
from mcp import Client
from mcp.server import MCPServer
mcp = MCPServer("Perceptron Lab invented manuals")
MANUALS = {"P7": "Inspect the mounting and coupling."}
@mcp.tool()
def read_manual(asset: str) -> dict:
"""Read the invented excerpt for a named pump."""
if asset not in MANUALS:
return {"found": False, "asset": asset}
return {"found": True, "asset": asset, "text": MANUALS[asset]}
@mcp.resource("manual://P7")
def p7_manual() -> str:
"""The invented P7 manual excerpt."""
return MANUALS["P7"]
@mcp.prompt()
def inspection_question(asset: str) -> str:
"""Ask which inspection the supplied manual supports."""
return f"Read the manual for {asset} and identify its inspection checks."
async def main():
# In-process transport for a local test, with no model or network call.
async with Client(mcp) as client:
listed = await client.list_tools()
assert "read_manual" in [tool.name for tool in listed.tools]
result = await client.call_tool("read_manual", {"asset": "P7"})
assert not result.is_error
print(result.structured_content)
if __name__ == "__main__":
anyio.run(main)The SDK builds the tool schema from the signature. The application still needs to handle tool failures and decide what content to trust.
Official API documentation. Documentation checked .
5. Read the result before treating it as evidence
A completed transport request can carry a tool failure. Inspect the result’s error status before reading structured data as a success. Also distinguish an application outcome such as found = False from a failure to contact the service at all.
The returned content may include text and other content blocks. Preserve provenance when adding it to context: which server returned it, from which record, and at what revision? Text that says to change the assistant’s rules is still returned content, not new authority over the host.
The typed client API uses its documented Python field names. A raw protocol JSON payload may spell fields differently, so do not interchange the two representations by guesswork.
6. Keep the configured server identity with the tool
Suppose the maintenance service and a community-notes service both expose read_manual. If the host indexes tools only by their local name, one can accidentally replace the other. Use an unambiguous mapping from the model-visible tool to its configured connection and local name.
The workshop writes that mapping as server/tool for readability. This slash convention is our local teaching registry, not an MCP naming requirement. The allowlist uses the same qualified identity, so permitting maintenance/read_manual does not also permit community/read_manual.
If a permitted tool disappears, report it as unavailable. Quietly falling back to a similarly named tool would change the source and possibly its authority.
Preserve the connection when indexing a discovered tool
The list holds two discovery records. Each dictionary has a server field and a local tool name. Constructing both labels before using a set keeps the two identities distinct.
discovered = [
{"server": "maintenance", "tool": "read_manual"},
{"server": "archive", "tool": "read_manual"},
]
qualified = [item["server"] + "/" + item["tool"] for item in discovered]
print(qualified)
print(len(set(qualified)))['maintenance/read_manual', 'archive/read_manual']
2Two distinct qualified names remain. A set containing only the local tool name would collapse these records and lose their connection identity.
7. A local server still runs real code
A server launched over standard input and output is a local program with the permissions of its process. Connecting it does not automatically confine its filesystem or network access. Review the program, its configuration and the permissions it receives.
A remote service brings another trust boundary. Imagine the maintenance server accepting a token intended for the payroll service just because the token has a valid signature. The signature tells us who issued it; we still have to check who may use it and where. An HTTP MCP server using the protocol’s authorisation scheme must validate that a token was issued for that server. It must not pass the incoming token through to a downstream API.
Keep credentials in the host or service environment. Grant the scopes needed for this job, and check object access inside the receiving service. Tool descriptions and annotations are claims from the server, not independent proof of its behaviour.
Start with a narrow read-only integration and invented fixtures. Add a write tool only after the approval and idempotency rules from the preceding lesson are enforced by the effect-owning service.
8. Test the connection without confusing it with task quality
A protocol test can establish that a tool is discoverable, accepts the expected arguments and returns the expected fixture. The workshop adds a host routing check, including a same-name tool on a different server. Those are useful integration tests.
A separate agent evaluation must ask whether Botie chose the right capability and used its output correctly. Keep both layers. When a technician receives an unsupported answer, we want to distinguish a misrouted lookup from an incorrect interpretation of a correctly retrieved excerpt.
The next work is observing a complete run so we can see where a failure began, rather than judging only the final sentence.
Check yourself
Questions on Parts 1 to 8
Question 1 of 2
What must the host preserve when routing?
Use the information below.
maintenance/read_manual and community/read_manual have the same local name. Only the maintenance endpoint is allowed.Try it with Botie
Build with Botie
Write and run Python