Agent Plugin Internals

Developer reference for the AI-assistant integration. For end-user installation see AI Assistant.

Layout

AGENTS.md                            # repo-wide agent instructions (Codex/Cursor/Claude read this)
.claude-plugin/marketplace.json      # makes the repo a Claude Code plugin marketplace
agent-plugin/
  .claude-plugin/plugin.json         # plugin manifest
  .mcp.json                          # registers both servers via ${CLAUDE_PLUGIN_ROOT}
  mcp/server.py                      # read-only knowledge server, stdlib only, Python 3.9+
  mcp/jobs_server.py                 # job driver (ti-toolbox-jobs), stdlib only, Python 3.9+
  mcp/stdio_loop.py                  # JSON-RPC stdio loop shared by both servers
  mcp/test_stdio.py                  # offline MCP wire smoke tests (unittest)
  skills/
    ti-toolbox/SKILL.md              # orientation (auto-loaded)
    ti-scripting/SKILL.md            # Python API cheat-sheet (auto-loaded)
    ti-domain/SKILL.md               # TI physics/neuroscience background (auto-loaded)
    ti-codebase/SKILL.md             # module graph and conventions (auto-loaded)
    troubleshoot-project/SKILL.md    # /ti-toolbox:troubleshoot-project <root> [subject]
    ti-run-pipelines/SKILL.md        # playbook for the job tools (auto-loaded)
  README.md
tests/test_agent_plugin_mcp.py
tests/test_agent_plugin_jobs.py

AGENTS.md at the repo root is the single source of project context for agents working on the codebase; the plugin is for agents working with the toolbox. Keep the two consistent when the architecture changes.

MCP server

agent-plugin/mcp/server.py implements JSON-RPC 2.0 over newline-delimited stdio (the MCP stdio transport) by hand — no SDK, no dependencies, so it runs on any user’s Python. The loop itself is agent-plugin/mcp/stdio_loop.py, shared with the job server: each tools/call runs on its own thread, with progress notifications, notifications/cancelled and a clean shutdown when stdin closes; each server puts its own directory on sys.path to import it, so it starts from any working directory.

Source resolution. On start it looks for a TI-Toolbox checkout: TI_TOOLBOX_ROOT, otherwise the first ancestor of server.py containing tit/ and docs/wiki/. With a checkout, every tool reads the working tree (so claude --plugin-dir ./agent-plugin from the repo gives you live source). Without one, files are fetched from raw.githubusercontent.com / the GitHub contents API at TI_TOOLBOX_REF (default main) and cached for 24 h in TI_TOOLBOX_CACHE (default ~/.cache/ti-toolbox-mcp).

Tools.

Tool Notes
get_quick_facts Static orientation blob; agents are told to call it first
list_wiki_pages, read_wiki_page, search_wiki Parse docs/wiki/*.md front matter; read_wiki_page can return a single ## section
read_changelog, get_toolbox_version docs/dev/CHANGELOG.md, tit/__init__.py, desktop/package.json
list_source_dir, read_source_file Restricted to approved source trees, including tit/, desktop/src/, desktop/tests/, contracts/, agent-plugin/, scripts and docs, plus selected manifests; text extensions only; path traversal rejected
read_dev_doc, list_launch_paths Read current developer references and launch-entry-point guidance
find_symbol, search_source Local checkout only (regex over the tree)
inspect_project, read_project_config Walk a user’s BIDS project using the same directory conventions as tit/paths.py; names only, no file contents except code/ti-toolbox/config/*.json

Every payload is capped at 60 kB. Tool failures are returned as isError: true results, never as crashes.

Adding a tool. Write a tool_<name>(args) -> dict function, append an entry to TOOLS with a JSON-schema inputSchema, and add a test. Keep this server’s tools read-only; anything that changes state belongs in the job server below.

Job server

agent-plugin/mcp/jobs_server.py (ti-toolbox-jobs) runs on the same stdio_loop.py and drives the running tit.server; it never runs science itself. It finds the server through TIT_SERVER_URL + TIT_SERVER_TOKEN when both are set, otherwise by docker ps --filter label=tit.stack=ti-toolbox-v3 and docker inspect (token and port from the container’s environment, the host project from its tit.host_project_dir label — the same rule as tit launch). The token is never returned to the agent.

Tools. connect, find_regions, get_config_schema, plan_job, propose_pipeline, watch_proposal, get_proposal, submit_job, wait_for_job, cancel_job and simulate_flex_result.

Default flow: propose, approve, run. connect reports approval_required, which is true unless the user turned on Settings ▸ Project ▸ AI assistant ▸ Agent may submit without approval. While it is true, the agent sends the whole pipeline to POST /api/proposals with propose_pipeline; the user approves, edits or rejects the plan card in the app, and the server queues the approved steps itself. watch_proposal (long-poll, returns on the next change) and get_proposal read /api/proposals/<id>. submit_job and simulate_flex_result are refused (HTTP 403) until direct submission is allowed.

Direct submissions go through POST /api/jobs/groups (per-subject kinds, as the run pages submit them) or POST /api/jobs (with after), carrying created_by: "agent" and only the fields the agent chose: the server fills the rest from the run pages’ own defaults (tit/server/app_defaults.py, served as x-app-defaults in GET /api/schema, which get_config_schema shows), on validate, plan and preflight too. find_regions is GET /api/catalog/regions passed through. A flex run’s output_folder is resolved to an absolute folder before submission, as the Optimizer does, so its run name is known up front. It writes no files on the host: raw scans are copied into sourcedata/ by the agent’s own file tools, under its CLI’s permission prompts. Tool annotations mark which tools are read-only and which may replace results.

Skills

Skills are Markdown with YAML front matter. user-invocable: false marks background knowledge the model loads by itself when the description matches; argument-hint is for slash-command skills. Inside a plugin they are namespaced as /ti-toolbox:<skill>.

agent-plugin/skills/ is the single source for these skills; .claude/ is git-ignored, so a local .claude/skills/ copy is only a convenience for your own Claude Code and never the version that ships.

Testing

python3 agent-plugin/mcp/server.py --selftest            # smoke test, local or remote mode
python3 -m unittest discover -s agent-plugin/mcp -p 'test_*.py' -v   # MCP wire framing (test_stdio.py)
pytest tests/test_agent_plugin_mcp.py -q                  # tools, path guards, fake BIDS project, stdio round-trip
pytest tests/test_agent_plugin_jobs.py -q                 # job server: fake server + one run against the real app
claude plugin validate agent-plugin                       # manifest check
claude plugin validate .claude-plugin/marketplace.json
claude --plugin-dir ./agent-plugin                        # run Claude Code with the working-tree plugin

To test remote mode, copy server.py and stdio_loop.py outside the repo and run --selftest; it should report repo root: (none -> GitHub).

Releasing

Bump version in agent-plugin/.claude-plugin/plugin.json when tools or skills change in a user-visible way. Users on the marketplace pick up changes with /plugin update ti-toolbox@ti-toolbox; the wiki/source themselves are read live, so documentation updates need no plugin release.

The desktop app ships its own copy of the plugin (electron-builder.yml copies agent-plugin/ into the app’s resources, without mcp/test_*.py), and its Assistant pane attaches that copy, so app users get the plugin of their installed release. It sets TI_TOOLBOX_REF to v<app version>, so the knowledge server reads the docs and source of that release rather than main. .mcp.json starts both servers with ${TIT_PYTHON:-python3}; for Claude Code the Assistant pane sets TIT_PYTHON to the Python it found (unless already set), and for Codex it passes that Python as the command.