10. Connecting Your Agent over MCP¶
Chapter 9 argued that a validated dictionary is the right interface for a language model. This chapter builds the plumbing that carries it: connect Claude Desktop, ChatGPT, Cursor or Claude Code to TuiML, so that asking for a model in English actually trains one.
You will learn:
- what MCP is and what it does not do
- the one command that wires up every AI client on your machine
- the manual configuration, for when you want to know what the command wrote
- how to verify the connection and read the tool surface
- exactly what leaves your machine, and what does not
Prerequisites: chapter 9.
import json
import tuiml
from tuiml.agent import get_workflow_tools, execute_tool, MCP_AVAILABLE
print("TuiML", tuiml.__version__, "| MCP support:", MCP_AVAILABLE)
TuiML 0.2.0 | MCP support: True
10.1 What MCP is¶
The Model Context Protocol is a standard for letting an AI assistant call software running on your machine. The assistant does not get code execution; it gets a list of named tools with JSON schemas, and the ability to call one with arguments. Your program decides what those tools do.
The arrangement has three parts:
You <--> Assistant (Claude, ChatGPT, ...) <--> MCP server (TuiML, local)
the model, remote your machine, your data
TuiML's server is a normal local process. It speaks MCP over stdin/stdout, and the client application starts it for you.
What this buys over pasting code into a chat window is that the assistant's suggestions actually run, against your real data, with results coming back for it to react to. The difference between "here is some code you could try" and "I tried it, here are the numbers" is the entire point.
10.2 The one command¶
TuiML ships a setup wizard that detects the AI clients installed on your machine and writes each one's configuration file for you.
tuiml setup
To see what it would touch, without changing anything:
tuiml setup --list
TuiML Setup Wizard
Connect TuiML to your AI agents
· Detecting installed AI clients ...
Detected:
· ✓ Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json)
· ✓ Cursor (~/.cursor/mcp.json)
· ✓ OpenAI Codex (CLI · IDE · ChatGPT Desktop) (~/.codex/config.toml)
Non-interactive, for a scripted install:
tuiml setup --yes # every detected client
tuiml setup --client claude-desktop --client cursor # only these
Supported clients include Claude Desktop, Claude Code, Cursor, OpenAI Codex (which is also how ChatGPT Desktop is wired), Windsurf, Zed, Continue, Perplexity Desktop, Gemini CLI and OpenCode. Each stores its MCP configuration in a different place and format; the wizard knows all of them.
Remark — restart the client afterwards. MCP configuration is read at startup. Claude Desktop in particular must be fully quit, not just closed to the dock, or the server will not appear.
10.3 The manual version¶
The wizard writes small files. It is worth knowing what they contain, both to debug and because you may be configuring a client the wizard does not know.
Claude Desktop, Cursor, and most JSON clients — add a mcpServers entry:
{
"mcpServers": {
"TuiML": {
"command": "tuiml-mcp",
"args": []
}
}
}
Config file locations:
| Client | Path |
|---|---|
| Claude Desktop (macOS) | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Claude Desktop (Windows) | %APPDATA%\Claude\claude_desktop_config.json |
| Cursor | ~/.cursor/mcp.json |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
| Zed | ~/.config/zed/settings.json (key is context_servers) |
OpenAI Codex / ChatGPT Desktop uses TOML instead:
[mcp_servers.TuiML]
command = "tuiml-mcp"
args = []
If tuiml-mcp is not on the client's PATH — common when TuiML lives in a virtualenv — give the absolute path:
{
"mcpServers": {
"TuiML": {
"command": "/Users/you/project/.venv/bin/tuiml-mcp",
"args": []
}
}
}
Remark — a GUI app does not inherit your shell
PATH. This is the single most common reason a correctly written config produces a server that never starts. If the wizard's entry works from a terminal client but not from Claude Desktop, use the absolute path.
The server can also be started by hand, which is how you check it runs at all:
tuiml-mcp # console entry point
python -m tuiml.agent.mcp # the same thing as a module
It will sit there waiting for MCP traffic on stdin. That silence is success; press Ctrl-C.
10.4 Verifying the connection¶
In the client, ask for something that has an unambiguous answer:
You: What TuiML tools do you have?
A working connection produces a list of tool names. A broken one produces the model's opinion about what TuiML probably does, which is worth learning to recognise — a model with no tools will often answer plausibly rather than admit it has none.
The sharper test names a tool:
You: Use tuiml_system_info and tell me exactly what it returns.
Here is that same tool surface, from Python:
tools = get_workflow_tools()
print(f"{len(tools)} tools exposed over MCP\n")
groups = {
"discovery": ["tuiml_list", "tuiml_describe", "tuiml_system_info"],
"data": ["tuiml_profile_data", "tuiml_read_data", "tuiml_upload_data",
"tuiml_generate_data", "tuiml_preprocess", "tuiml_select_features"],
"modelling": ["tuiml_train", "tuiml_tune", "tuiml_predict", "tuiml_evaluate",
"tuiml_benchmark", "tuiml_test_statistics", "tuiml_plot"],
"deployment": ["tuiml_save_model", "tuiml_serve_model", "tuiml_stop_server",
"tuiml_server_status"],
"authoring": ["tuiml_get_skeleton", "tuiml_create_algorithm",
"tuiml_edit_algorithm", "tuiml_read_algorithm",
"tuiml_search_source", "tuiml_list_files",
"tuiml_delete_algorithm"],
}
for group, names in groups.items():
print(f"{group}:")
for name in names:
print(f" {name}")
other = set(tools) - {n for names in groups.values() for n in names}
print(f"\nalso: {', '.join(sorted(other))}")
30 tools exposed over MCP
discovery:
tuiml_list
tuiml_describe
tuiml_system_info
data:
tuiml_profile_data
tuiml_read_data
tuiml_upload_data
tuiml_generate_data
tuiml_preprocess
tuiml_select_features
modelling:
tuiml_train
tuiml_tune
tuiml_predict
tuiml_evaluate
tuiml_benchmark
tuiml_test_statistics
tuiml_plot
deployment:
tuiml_save_model
tuiml_serve_model
tuiml_stop_server
tuiml_server_status
authoring:
tuiml_get_skeleton
tuiml_create_algorithm
tuiml_edit_algorithm
tuiml_read_algorithm
tuiml_search_source
tuiml_list_files
tuiml_delete_algorithm
also: tuiml_export_notebook, tuiml_restart, tuiml_self_update
The authoring group is the surprising one. An agent is not limited to the algorithms that shipped: tuiml_create_algorithm accepts Python source for a new @classifier or @regressor, validates it against a conservative denylist, stores it under ~/.tuiml/user_algorithms/, and registers it. After that it is an ordinary catalog entry, and every other tool works on it unchanged.
Remark — that denylist is a guard, not a sandbox. Source accepted by
tuiml_create_algorithmruns in your interpreter with your permissions. It blocks the obvious mistakes an agent might make; it is not a defence against a determined attacker who controls the model's input.
10.5 What a tool looks like¶
Each tool is a name, a description, and a JSON Schema. This is everything the model sees:
schema = tools["tuiml_train"]
print("name:", schema["name"])
print()
print("description:", schema["description"][:220], "...")
print()
print("parameters:")
for param, meta in list(schema["inputSchema"]["properties"].items())[:8]:
print(f" {param:16s} {meta.get('type', '?'):8s} "
f"{(meta.get('description') or '')[:44]}")
print()
print("all parameters:", ", ".join(schema["inputSchema"]["properties"]))
name: tuiml_train description: Train a machine learning model with evaluation. Two evaluation modes: 1. Holdout (default): splits data into train/test sets using test_size. Returns metrics on the test set and predictions. 2. Cross-validation: set cv=5 ... parameters: algorithm string Algorithm class name. Examples: - Classifier data string Data file path or built-in dataset name (e.g target string Target column (required for supervised, opti features array Optional: restrict the feature matrix to the preprocessing array Preprocessing steps as names or objects with feature_selection ? Feature selection method. String name or obj cv integer Number of cross-validation folds (e.g. 5 or test_size number Proportion of data for the test set (0.0-1.0 all parameters: algorithm, data, target, features, preprocessing, feature_selection, cv, test_size, metrics, preset, algorithm_params, save_path, random_seed, stage, stage_kwargs, model_id, model_path
The tool covers the same ground as chapter 9's spec, but flattened: where the spec nests {"model": {"name": ..., "params": {...}}}, the tool takes algorithm and algorithm_params as sibling arguments, and pipeline becomes preprocessing alongside preset, cv, test_size and metrics.
The flattening is deliberate. Tool-calling APIs constrain a model to a single flat argument object far more reliably than to arbitrary nesting, so the surface an agent writes to is a form to fill in rather than a document to compose. Underneath, the tool assembles a spec and calls tuiml.train() — the same function, reached the same way, with the same validation.
Remark — note that
requiredis empty. Every argument has a default, so a call with no arguments at all is valid. That makes the tool forgiving of a model that omits something, at the cost of letting an underspecified call succeed quietly. It is one more reason chapter 11 is about checking what actually ran rather than trusting the summary.
10.6 The same code path¶
An MCP tool call and a Python call go to the same place. execute_tool runs one directly, which is how you debug what an agent saw:
profile = execute_tool("tuiml_profile_data", data="diabetes")
print("keys returned :", ", ".join(profile))
print()
print("shape :", profile["n_samples"], "x", profile["n_features"])
print("class balance :", profile["class_distribution"])
print("declared missing:", profile["missing_values"] or "none")
print()
print("per-column stats the agent receives:")
for column in ["preg", "plas", "insu", "mass"]:
stats = profile["numeric_stats"][column]
print(f" {column:6s} min={stats['min']:6.1f} max={stats['max']:6.1f} "
f"mean={stats['mean']:6.1f}")
keys returned : status, name, shape, n_samples, n_features, feature_names, dtypes, missing_values, numeric_stats, class_distribution, random_seed
shape : 768 x 8
class balance : {'0': 500, '1': 268}
declared missing: none
per-column stats the agent receives:
preg min= 0.0 max= 17.0 mean= 3.8
plas min= 0.0 max= 199.0 mean= 120.9
insu min= 0.0 max= 846.0 mean= 79.8
mass min= 0.0 max= 67.1 mean= 32.0
Two things in that output are worth reading together.
missing_values is empty — the dataset declares nothing missing. But the minimums for plas, insu and mass are all 0.0, which chapter 1 established is physiologically impossible. The profile hands an assistant both facts at once, before it has proposed anything, and the contradiction between them is exactly the signal a competent analyst acts on.
It does not need to be told the dataset is dirty. It needs to notice — and the next chapter is largely about whether it did.
10.7 What crosses the network¶
Worth being precise about, because "connect an AI to your data" reasonably makes people nervous.
The MCP server is a local process. It reads your files, holds your arrays, and trains your models entirely on your machine.
What goes to the model provider is the conversation: your messages, the tool names and arguments the model chooses, and whatever the tool returns.
That last clause is the one that matters. Tool results are part of the conversation, so they do go to the provider — and some of them contain data:
| Tool | What comes back |
|---|---|
tuiml_train |
metrics, model id — no rows |
tuiml_list, tuiml_describe |
catalog metadata — no rows |
tuiml_profile_data |
summary statistics — means, minimums, class counts, column names |
tuiml_read_data |
actual rows |
So: your dataset is not uploaded, and a normal train-and-compare session moves only metrics and column names. But tuiml_profile_data sends aggregates, and tuiml_read_data sends records. On genuinely sensitive data, know which tools your assistant is reaching for — and prefer a local model if the aggregates themselves are confidential.
10.8 When it does not work¶
| Symptom | Cause |
|---|---|
| No TuiML tools in the client | Client not restarted, or config in the wrong file |
| Server "fails to start" in a GUI app | tuiml-mcp not on the app's PATH — use the absolute path |
| Tools appear but every call errors | TuiML installed in a different environment than the one being launched |
| Model describes tools it never calls | It has no tools and is improvising — check the tool list explicitly |
MCP_AVAILABLE is False |
pip install "tuiml[mcp]" |
To see the traffic, run the server by hand in a terminal and watch it while the assistant works:
tuiml-mcp
Recap¶
- MCP lets an assistant call local tools; TuiML's server is a normal local process speaking it over stdio.
tuiml setupdetects your AI clients and writes their configuration.--listshows what it would touch.- Manual config is a small
mcpServersJSON block, or a[mcp_servers.TuiML]TOML table for Codex/ChatGPT. - A GUI app does not inherit your shell
PATH— the most common failure. Use an absolute path totuiml-mcp. - 30 tools, grouped into discovery, data, modelling, deployment and authoring.
- Every tool is a name plus a JSON Schema.
tuiml_trainexposes chapter 9's spec flattened into sibling arguments, because tool-calling APIs handle flat objects more reliably than nested ones. execute_tool(...)runs a tool from Python, on the same code path the agent uses.- Your data stays local. Tool results go to the provider —
profile_datareturns statistics,read_datareturns rows.
Next: chapter 11 has a real conversation with the connected agent, and then checks its work.