T14-L04 · Connect your tools · Level 4 Integrator · 21-minute read, plus 3 hours of doing
The system without a connector
In a lab, Birgit's Monday brief (from T14-L03) runs well. Now the group wants more from the assistant. Every week someone asks "Do we still have the anti-GFP antibody, and where is it?" The answer lives in the institute's electronic lab notebook (ELN), eLabFTW, which also keeps the group's inventory: every reagent, where it's stored, how much is left. Claude has connectors for SharePoint and Teams. It has none for eLabFTW.
In a company, Lea's colleagues in sales spend twenty minutes a day on one question from customers: "Is it in stock, and when is the next delivery?" The answer is in the company's stock system, Northstar Inventory, which has a web interface and an API, but no connector. A colleague suggests giving the assistant a database login "so it can look things up".
That suggestion is the problem this book solves. A database login lets the model ask anything. A connector you build lets it ask one question, safely.
You are at Level 4 Integrator. A connector touches a real system that other people's work depends on. If it can write, a model's mistake changes real records. If it can read too much, it hands data to people who shouldn't see it.
After this you can
- Decide what one connector tool may do, with which key and which fields, before you write code.
- Build a small MCP server in Python with one read-only tool and connect it to Claude Desktop.
- Give the connector the smallest access that works: a read-only key per person, and only the fields the task needs.
- Run six tests, including a write attempt that must fail and a run by a colleague with different access.
- Write a README with the stop, the fallback and who signed off, so the connector can be switched off in a minute.
Before you start
- Books: T01-L01 for the four-quadrant method. T14-L03 for connectors that read as whoever runs them, and a test sheet. T12-L04 for least privilege and proving limits by trying to break them.
- A system with an API, the part of a system that programs talk to, and its documentation. You need to know how to get a key and which requests read.
- The system's administrator (often in IT), who can tell you what a read-only key can see and must agree to the connector.
- Claude Desktop on your computer, on your organisation's plan, and permission to run a small program on it.
- Python, uv and Node.js. uv is a tool that installs Python packages and runs Python programs in one step. Node.js, the program that runs JavaScript outside a browser, is needed only for the test window you'll use. IT may need to install both.
- About 3.5 hours: 21 minutes to read, about 3 hours of doing.
Last verified 23 September 2026. The MCP Python SDK is at version 2 and free; its server class and imports changed from version 1, so older tutorials won't run unchanged. eLabFTW lets each user create API keys that can only read. Details for experts at the end of this book.
The idea in one page
A connector is a small program that sits between an assistant and a system. The standard way to build one is MCP, the Model Context Protocol: a common format that lets assistants such as Claude Desktop discover a program's tools and call them. The program is called an MCP server, even when it runs on your own laptop.
The model decides when to call a tool and what to pass it. Your code decides what's allowed. Build from that boundary inward:
model asks ──► one named tool ──► input checked ──► one read request
with a read-only key
│
answer with the few fields the task needs ◄──────┘
Five decisions make a connector safe. Write them down before any code:
| Decision | Good choice | What goes wrong otherwise |
|---|---|---|
| One job per tool | find_stock(name) | A general tool like query(sql) lets the model ask anything the key allows |
| Checked input | A name of 3 to 60 characters | Odd input reaches the system, and error messages reveal how it works |
| A read-only key | A key the system itself refuses to write with | A prompt saying "never write" is advice, not a lock |
| Whose key | Each person's own key, if the system allows it | A shared key shows everyone what the key can see |
| Few fields | Name, place, amount | A whole record hands over notes, prices or names nobody asked for |
The third row is the one to remember. The model's instructions are text. The key is the lock. You don't write a write tool, and you use a key that couldn't write even if you did.
Tool descriptions are hints, not security. MCP lets you mark a tool as read-only, and a well-behaved assistant uses that to decide whether to ask the user first. But the documentation of the MCP Python SDK (software development kit, the official library for writing MCP servers in Python) says it plainly: never rely on a client honouring those hints.
It runs on each person's computer. Claude Desktop starts the connector itself, as a program it owns, and talks to it over the program's input and output, a way of connecting called stdio. Nothing listens on the network; closing Claude Desktop stops it. That's right for a few colleagues. A connector for a whole organisation, run on a server with central sign-in, is Level 5 work (T14-L05).
Who signs off: the system's administrator agrees that the connector may use the API and confirms what a read-only key can see. IT agrees that the connector may run on work computers. Whoever answers for the data (the PI, the principal investigator who leads the group, in a lab; the head of sales in the company) agrees which fields may reach the assistant. If any field is personal data, ask the data protection officer (DPO) too.
Worked example: two connectors, four quadrants
The method comes from T01-L01. Here it is for Birgit's inventory question, then for Lea's stock question.
Lab: where is the antibody, and how much is left?
Structure it. Birgit: "Anyone on the rota can ask the assistant where a reagent is stored and how much is left, from the group's inventory in eLabFTW. It must never change the inventory, and it must never show another group's items. A useful result: a connector three of us use, a test sheet that passes, and the ELN administrator's written OK."
Understand it. She reads eLabFTW's API documentation and finds the two requests she needs. One searches the group's items by name; the other lists where one item is stored, with the amount left. Everything else in the API (creating, changing, deleting, uploading) the connector will never call.
Then she meets Jens, who administers the institute's eLabFTW, with two questions. Can a key be limited to reading? Yes: each user creates their own API keys on eLabFTW's Settings page, giving each a name and an access level, and the access level can be read-only. What does a key see? Exactly what its user sees, because the key belongs to one account.
That second answer changes her plan. She had pictured one key for the connector, created under her own account, shared with Oskar and Mira. But her account can see the inventory of a collaborating group too, from a shared project. Through her key, Oskar's assistant would show him items he can't open himself. Back to Structure it: "Each person uses their own read-only key. The connector sees what that person sees, and nothing more, like the Monday brief."
She also looks at what an inventory record holds. Besides the name, storage place and amount, items carry a free-text body: protocols, supplier notes, and on one item an old note with a collaborator's phone number. None of that answers where is it and how much is left, so none of it leaves the connector.
Choose tools.
- The MCP Python SDK, because Python is what the group already uses, and the SDK builds the tool description from ordinary function code. The TypeScript SDK is the alternative for teams who work in JavaScript.
- Claude Desktop as the assistant that runs it, because the group already uses Claude on a Team plan (T14-L03) and Claude Desktop can start local MCP servers.
- httpx, a Python library for web requests that comes with the SDK, for the two calls to eLabFTW.
- The MCP Inspector, a test window that comes with the SDK, to call the tool by hand before any model does.
Build it. She writes the tool contract into the README first:
Tool: find_stock(name)
Does: finds up to 5 inventory items matching a name and says
where each is stored and how much is left
Input: name, 3-60 characters
Returns: item, where, amount. Nothing else.
Key: each user's own eLabFTW API key, created as READ ONLY
Never: create, change, delete, move, upload; item text or notes
Then the server, one file called inventory.py:
import os
from typing import Annotated
import httpx
from pydantic import Field
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import ToolAnnotations
ELN = os.environ["ELN_URL"] # e.g. https://eln.example.org/api/v2
KEY = os.environ["ELN_API_KEY"] # the user's own read-only key
mcp = MCPServer("Lab inventory")
def eln_get(path, **params):
try:
r = httpx.get(ELN + path, params=params,
headers={"Authorization": KEY}, timeout=10)
except httpx.HTTPError:
raise ToolError("The ELN isn't reachable. Check it in the browser.")
if r.status_code in (401, 403):
raise ToolError("The ELN refused this key. Ask the lab manager.")
r.raise_for_status()
return r.json()
@mcp.tool(annotations=ToolAnnotations(read_only_hint=True,
open_world_hint=False))
def find_stock(
name: Annotated[str, Field(min_length=3, max_length=60,
description="Name of a reagent, antibody or kit")],
) -> list[dict]:
"""Find up to 5 items in the group's lab inventory by name, and say
where each is stored and how much is left. Read-only."""
items = eln_get("/items", q=name, limit=5)
if not items:
raise ToolError(f"No inventory item matches {name!r}.")
found = []
for item in items:
for place in eln_get(f"/items/{item['id']}/containers"):
found.append({"item": item["title"],
"where": place["full_path"],
"amount": f"{place['qty_stored']} {place['qty_unit']}"})
return found
if __name__ == "__main__":
mcp.run()
Read it in three parts.
The top loads the libraries. Besides the SDK and httpx, it uses pydantic, which the SDK uses to check input, for the Field rule further down. Then it reads two settings from the environment: the address of eLabFTW's API and the person's key. Neither is written in the file, so the file can be shared and kept in Git.
eln_get makes one read request. When eLabFTW can't be reached or refuses the key, it raises a ToolError, which the SDK passes to the model as a short message it can relay. The message says what to do and reveals nothing about the system. Any other error status stops the call too, and the model only learns that it failed.
find_stock is the only tool. Its name, the description in quotes and the typed input become what the assistant sees. The Field line rejects a name under 3 or over 60 characters before any request is made. The read-only mark is set, as a hint. And the result is built field by field: item, place, amount. The item's text never enters it.
Before the first run she creates a project folder with uv init and installs the SDK into it with uv add "mcp[cli]". To try the server by hand, she starts it in the Inspector:
uv run mcp dev inventory.py
This opens a local test page where she can call find_stock with any input and see exactly what comes back. Then she connects it to Claude Desktop:
uv run mcp install inventory.py -v ELN_URL=https://eln.example.org/api/v2 -v ELN_API_KEY=<your read-only key>
The command writes an entry into Claude Desktop's settings file with the two values, so Claude Desktop can start the server itself. After a full restart of Claude Desktop, the tool appears in the list of connectors. The key now sits in that settings file on her laptop, like a saved password, so it must be her own read-only key and nobody else's.
Oskar and Mira do the same on their own laptops. Each creates a read-only key on their eLabFTW Settings page, sets up a uv project with the same two commands, copies inventory.py into it from the group's Git repository, and runs the install command with their own key.
Build it: six tests. She runs each case and writes the result on the test sheet:
| # | Case | Expected | Result |
|---|---|---|---|
| 1 | "anti-GFP" in the assistant | Two places, with amounts | Pass |
| 2 | "zebrafish" (not in the inventory) | "No inventory item matches" | Pass |
| 3 | "ab" in the Inspector | Rejected before any request | Pass |
| 4 | Ask the assistant to "set anti-GFP to 0 ml" | No tool can do it; the assistant says so | Pass |
| 5 | A direct write request with the same key, sent by hand | eLabFTW refuses it | Pass |
| 6 | Oskar asks for an item of the collaborating group | Not found for him; found for Birgit | Pass |
Test 5 is the one that makes the rule true. With the Inspector closed, she sends one change request to eLabFTW's API with her connector key, aimed at a test item Jens created. She uses curl, the command-line tool that sends a single web request; PATCH is the request type that changes a record:
curl -X PATCH -H "Authorization: <read-only key>" -H "Content-Type: application/json" -d '{"title": "write test"}' https://eln.example.org/api/v2/items/<test item id>
eLabFTW refuses it with an error, because the key is read-only, and the test item keeps its name. If it had worked, she'd have known the key was wrong, and only a test item would have changed.
Test 6 is the one from T14-L03, in a new place: the connector reads as whoever runs it. Oskar's key shows him his group's items and nothing else.
Build it: sign-off and stop. The README gets four more lines: who maintains it (Birgit, deputy Oskar), how to stop it (delete your key in eLabFTW settings; the tool fails at once), the fallback (look it up in eLabFTW in the browser), and the sign-offs. Jens agrees in writing that read-only keys may be used this way. The PI agrees that item names, places and amounts may go to the group's assistant; they're Group class (material everyone in the group may see), not personal data.
What it taught her: the code took an hour. The decision that mattered came from one question to the administrator, what does a key see?, and it turned one shared key into three personal ones.
Company: is it in stock, and when is the next delivery?
Structure it. Lea: "The three of us in inside sales can ask the assistant whether an item is in stock and when the next delivery arrives, from Northstar Inventory. It must never reserve, order or change anything, and it must never show purchase prices. A useful result: a connector we use, a passing test sheet, and IT's written OK."
Understand it. Stefan from IT shows her the stock system's API documentation and its roles. Every API token belongs to a person and inherits a role, a named set of permissions. The existing Sales role can reserve stock for a customer and see purchase prices, because the sales team needs both in the web interface.
That's too much for a connector. Back to Structure it: "The connector needs its own, smaller role. It can read stock level, warehouse and next delivery date. It can't reserve or order, and the API doesn't return prices to it at all."
Stefan creates the role Stock read, and issues each of the three a personal token with that role, valid for 90 days. Lea checks what a stock record returns under the new role: item number, name, stock level, warehouse, next delivery date, and a list of open reservations with customer names. The customer names are personal and commercial data that the question doesn't need.
Choose tools. The same as Birgit's: the MCP Python SDK, Claude Desktop (the sales team uses Claude on the company's Team plan), and the Inspector. Stefan considered a no-code integration platform instead, but it would have meant sending tokens to another cloud service.
Build it. Her server has the same shape as Birgit's, with one tool, check_stock(item_number). The input is stricter, because item numbers have a fixed form: Field(pattern=r"^NS-\d{6}$") accepts only NS followed by six digits, so the model can't pass a name, a wildcard (a pattern such as * that matches many items) or anything else. The result has four fields: item name, in stock, warehouse, next delivery. The reservation list never enters it.
Her six tests follow Birgit's pattern. Two are specific to the company. In test 4 she asks the assistant to "reserve 20 of NS-104233 for Hartmann". The assistant has no tool for it and says so; she checks in the stock system that nothing was reserved. In test 5 she sends one reservation request by hand with her connector token: the stock system refuses it, because Stock read can't reserve.
She adds a seventh test for failure. With Stefan's help she points the connector at the test system while it's switched off. The assistant answers "The stock system isn't reachable", and doesn't guess a number. The README's fallback line says: look it up in Northstar's web interface, as before.
Build it: sign-off and stop. Stefan signs the README. The token expiry is in his calendar, and so is a line on the leaving checklist: a leaver's token is revoked on their last day. The stop is one action: Stefan disables the Stock read role, and every connector stops at once. Because no personal data reaches the assistant, the DPO only needs to be told, not asked; the note is in the README.
What it taught her: the connector wasn't the risky part. The existing sales role was. Building the connector made the team notice how much every sales token could already do.
In both places the safe connector was the smaller one: one tool, one read-only key per person, and fewer fields than the system offered.
What goes wrong
A general tool. Symptom: the connector has a query or request tool that passes whatever the model writes to the system. Fix: one tool per question, with typed input. If a new question comes up, add a new narrow tool after the same tests.
One shared key. Symptom: everyone's assistant sees what the person who created the key sees. Fix: each person uses their own key, if the system allows it. If it doesn't, ask the administrator for a service account with its own small role, and test what it sees.
"Read-only" in the prompt only. Symptom: the instructions say "never change anything", and the key can write. Fix: a key the system itself won't write with, proved by a write attempt that fails.
The whole record returned. Symptom: answers include notes, prices or names nobody asked for. Fix: build the result field by field. Anything not in the list stays in the system.
Errors that explain the system. Symptom: a failed call shows a stack trace (the program's internal error report), the database name or the key. Fix: catch errors in the connector and return a short message that says what to do.
A key in a shared file. Symptom: the key is in the Python file or in Git, and the whole team has a copy. Fix: the key comes from the environment, set per person. If one leaks, delete it in the system and create a new one.
Nobody can switch it off. Symptom: the connector misbehaves while its builder is away. Fix: the README names the stop (delete the key, disable the role) and who can do it, and one other person has tried it.
Do it yourself (3 hours)
- Structure it (15 min). Write one sentence: which question, which system, who asks, and what the connector must never do. Add what a useful result looks like.
- Understand it (40 min). Read the system's API documentation and find the one or two requests that answer the question. Ask the administrator: can a key be read-only, whose permissions does it carry, and what does a record contain? Rewrite your sentence if the answers change it.
- Choose tools (10 min). Pick the SDK and the assistant that will run the connector. Decide whose key it uses and which fields it returns.
- Build it (70 min). Write the tool contract. Write the server with one tool, checked input and a result built field by field. Try it in the Inspector, then connect it to your assistant.
- Build it: test (35 min). Run the six tests: an allowed read, nothing found, bad input, a write request through the assistant, a write request by hand with the same key, and a colleague's run with different access. Add a failure test if you can switch off a test system.
- Build it: sign off (10 min). Write the README: maintainer and deputy, the stop, the fallback, the key rules and the sign-offs. Get the administrator's agreement in writing.
Exit check
Passing test: a working read-only connector, its README and a test sheet.
- The connector has one or two narrow read tools, checked input, a result with only the fields the task needs, and a key that comes from the environment.
- The test sheet has at least the six cases above, each with expected and actual result.
- The README names the maintainer and deputy, the stop, the fallback, whose keys are used, and who agreed.
It passes when all six cases give the expected result, the write attempt by hand is refused by the system itself, a colleague's run shows only what their own access allows, and the system's administrator has agreed in writing.
Rule to remember
The model's instructions are text; the key is the lock.
Details for experts
Checked on 23 September 2026. You don't need this to follow the book; it's for setting things up or answering a colleague.
| What | Details | What it means for you |
|---|---|---|
| MCP Python SDK | Package mcp, version 2.2.0 (7 September 2026), Python 3.10 or newer. Install with uv add "mcp[cli]" or pip install "mcp[cli]"; the cli extra adds the mcp command. Version 2 uses from mcp.server import MCPServer; version 1 tutorials use FastMCP. | Pin the version in your project so a colleague's install matches yours. |
| Running and testing | mcp.run() with no arguments starts a stdio server. mcp dev opens the Inspector, which needs Node.js, the JavaScript runtime, on the computer. mcp install writes the entry into Claude Desktop's settings file (claude_desktop_config.json), with -v NAME=value or -f .env for settings. Fully quit and reopen Claude Desktop afterwards. | If the tool doesn't appear, run the launch command yourself and read the error. |
| Errors | Raise ToolError for failures the model should see and explain. Any other exception is reported to the model only as a failed call. | Catch the system's errors and turn them into short, safe messages. |
| Tool annotations | read_only_hint (changes nothing), open_world_hint (reaches an open-ended outside world such as the web), destructive_hint (may delete or overwrite) and idempotent_hint (calling twice does no more than once) describe a tool's behaviour to the assistant. The SDK documentation calls them "hints, not security". | Set them, but rely on the key. |
| eLabFTW API | Version 2 at /api/v2. The key goes in the Authorization header. GET /items?q=…&limit=… searches items; GET /items/{id}/containers lists storage places with qty_stored, qty_unit, storage_name and full_path. Keys are created per user in Settings, as read-only or read-and-write. Current eLabFTW release: 6.0.4. | Each user creates their own read-only key. |
| Claude Code, Cursor, VS Code | The same server can be added to these tools with their own settings; only the settings file and the name of the section that lists servers differ. | One server, several assistants, same tests. |
Further reading and tools
Next books
- T14-L05: running connectors for a whole organisation, with central sign-in and monitoring.
- T14-L03: a routine that reads as whoever runs it, and its test sheet.
- T12-L04: least privilege and service accounts on a self-hosted service.
- T07-L04: the same limits when an agent chooses its own actions.
- T08-L03: writing the contract before the code.
Tools
- Claude Desktop: Taught. Runs the connector and shows its tool.
- Claude: Catalogued. The group's and the sales team's assistant plan.
- Claude Code: Catalogued. Can run the same connector for developers.
- n8n: Catalogued. When the question should be answered on a schedule rather than in a chat.