MCP Integration - ericfitz/tmi GitHub Wiki
TMI does not run an MCP server itself. Instead, tmi-mcp is a separate, client-side Model Context Protocol server that runs on your machine and lets AI agents (Claude Code, Codex, Grok Build) read and update threat models on any TMI server through the normal REST API.
- Repository: github.com/ericfitz/tmi-mcp
- Releases: github.com/ericfitz/tmi-mcp/releases (current: v1.0.1)
- Delivers GitHub issue #69, TMI as an MCP server (closed as fixed by tmi-mcp).
tmi-mcp is a local Go program that speaks MCP over stdio and wraps the generated TMI Go client. Your agent harness starts it as a subprocess; the first tool call opens a browser for an OAuth login, and the session then persists across restarts. Because it only calls the public REST API, no changes are needed on the TMI server beyond the OAuth callback allowlist described in Server Requirement.
When it connects, tmi-mcp sends usage instructions to the harness, so the agent learns how the tools fit together without extra setup.
macOS, with Homebrew (signed and notarized binary):
brew install ericfitz/tap/tmi-mcp
tmi-mcp initElsewhere, with Go:
go install github.com/ericfitz/tmi-mcp/cmd/tmi-mcp@latest
tmi-mcp inittmi-mcp version prints the installed version. Upgrade a Homebrew install with brew upgrade tmi-mcp.
tmi-mcp init registers the server, named tmi, with every agent harness it finds (Claude Code, Codex and Grok Build, detected by ~/.claude, ~/.codex and ~/.grok) using each harness's own CLI. It is safe to rerun. If a harness's CLI is not on PATH, it prints the config snippet to add by hand; if you have no tmi-mcp config file yet, it prints a sample. Restart the harness afterward.
-
--harness claude|codex|grokconfigures only that harness, even if it is not detected. -
--dry-runprints the commands without running them.
To register by hand, or to pin a profile:
claude mcp add -s user tmi -- tmi-mcp
claude mcp add -s user tmi -- tmi-mcp --profile localtmi-mcp reads ~/.config/tmi-mcp/config.yaml (override with --config; XDG_CONFIG_HOME is honored). It holds named profiles, one per TMI server:
default_profile: prod
profiles:
prod:
server: https://api.tmi.dev
idp: google
local:
server: http://localhost:8080
idp: tmi
login_hint: alice| Setting | Meaning |
|---|---|
default_profile |
Profile used when none is specified |
profiles.<name>.server |
Base URL of the TMI server |
profiles.<name>.idp |
OAuth identity provider on that server (for example google). The tmi provider is dev-only, for local TMI servers and test users |
profiles.<name>.login_hint |
Optional user hint passed to the provider (for example alice on a dev server) |
profiles.<name>.callback_port |
Optional, 1024-65535. Pins the OAuth loopback listener to this port instead of a random one |
--profile overrides default_profile for the process, and any tool call can override both with its own profile argument. auth with action=list_profiles shows the configured profiles.
Login uses an OAuth authorization-code flow with PKCE and a loopback redirect (http://127.0.0.1:<port>/...) on your machine. The TMI server must allow that callback URL, or login fails immediately with an error naming the missing entry.
api.tmi.dev allows loopback callbacks on any port (http://127.0.0.1:*, the RFC 8252 loopback pattern), so no callback_port is needed there. Operators of other TMI servers add the entry through the auth.oauth.client_callback_allowlist setting (environment variable TMI_OAUTH_CLIENT_CALLBACK_ALLOWLIST, a JSON list). This setting is database-backed and editable at runtime by administrators; see Configuration-Reference and Configuration-Management.
On a server with the http://127.0.0.1:* entry, the random port works. To allow a single fixed port instead, set callback_port on the profile (for example 8765) and add http://127.0.0.1:8765/* to the allowlist. Current TMI servers treat http://127.0.0.1:* as a loopback any-port pattern and always reject callback URLs with userinfo. Do not add that entry to an older server whose matcher is a plain string-prefix check, because http://127.0.0.1:* would also match hosts such as http://127.0.0.1:[email protected]/; see the tmi-mcp README.
Every tool takes an optional profile argument. fields is a JSON object; for update actions, each top-level key in fields becomes a JSON Patch add at /<key>, so omitted fields are never touched.
| Tool | Actions | Notes |
|---|---|---|
auth |
login, logout, refresh, whoami, list_profiles
|
refresh falls back to a browser login if the token cannot be refreshed; whoami calls GET /oauth2/userinfo
|
threat_models |
list, get, create, update
|
list filters: name, owner, status, security_reviewer, limit, offset. Threat models cannot be deleted |
threats |
list, get, create, update, delete
|
Scoped by threat_model_id
|
diagrams |
list, get, create, update, delete, get_model
|
get_model returns a compact node/edge model |
assets |
list, get, create, update, delete
|
Scoped by threat_model_id
|
documents |
list, get, create, update, delete
|
Scoped by threat_model_id
|
notes |
list, get, create, update, delete
|
Scoped by threat_model_id
|
repositories |
list, get, create, update, delete
|
Scoped by threat_model_id
|
metadata |
list, get, set, delete
|
target is one of threat_model, threat, diagram, asset, document, note, repository; set upserts by key |
projects |
list, get, create, update, delete
|
Top-level |
teams |
list, get, create, update, delete
|
Top-level |
List actions (except metadata) page with limit and offset.
- Lazy login: the first tool call opens a browser for the OAuth login; nothing happens until then.
-
Token storage: OS keychain (service
tmi-mcp, account = profile name). If the keychain is unavailable, tokens fall back to~/.config/tmi-mcp/tokens/<profile>.json(mode 0600, in a mode 0700 directory). Tokens are never logged. - Automatic refresh: on a 401 the token is refreshed and the call retried once; if refresh fails, tmi-mcp falls back to a browser login.
-
Concurrent sessions (v1.0.1+): TMI refresh tokens are single-use, so sessions that share a profile serialize login, refresh and logout with a file lock. Concurrent 401s share one refresh, and logout waits for any in-flight login or refresh. Upgrade with
brew upgrade tmi-mcpif you run several agent sessions against the same profile.
For a conversational assistant planned as part of TMI itself, see Timmy-AI-Assistant. TMI using external MCP servers (issue #70) was closed as not needed, given TMI's REST API, webhooks and addons.
The Model Context Protocol is an open standard for connecting AI assistants to data sources and tools.
Resources:
Besides tmi-mcp, you can integrate AI tools with TMI directly using:
-
REST API: Use TMI's REST API to build custom AI integrations (example: the TMI Terraform analyzer, see TMI-Terraform-Analyzer)
- See REST-API-Reference for API documentation
- Use generated SDKs for your language
-
Webhooks: Receive real-time notifications for AI processing
- See Webhook-Integration for setup
- Process threat model changes with AI services
-
Addons: Build AI-powered addons using the addon system
- See Addon-System for details
- Invoke AI analysis on-demand
A minimal example of calling the REST API from your own code:
import requests
import openai
# Configuration
TMI_API = 'https://your-tmi-server'
TMI_TOKEN = 'your-tmi-token'
OPENAI_KEY = 'your-openai-key'
def analyze_threat_model_with_ai(threat_model_id):
"""Use AI to analyze a threat model"""
# Fetch threat model from TMI
response = requests.get(
f'{TMI_API}/threat_models/{threat_model_id}',
headers={'Authorization': f'Bearer {TMI_TOKEN}'}
)
threat_model = response.json()
# Fetch associated threats (nested under the threat model)
response = requests.get(
f'{TMI_API}/threat_models/{threat_model_id}/threats',
headers={'Authorization': f'Bearer {TMI_TOKEN}'}
)
threats = response.json()
# Format for AI analysis
context = f"""
Threat Model: {threat_model['name']}
Description: {threat_model.get('description', 'N/A')}
Identified Threats:
{format_threats(threats)}
Please analyze this threat model and suggest:
1. Any missing threats
2. Improvements to existing threats
3. Prioritization recommendations
"""
# Call OpenAI
client = openai.OpenAI(api_key=OPENAI_KEY)
response = client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a security expert analyzing threat models."},
{"role": "user", "content": context}
]
)
analysis = response.choices[0].message.content
return analysis
def format_threats(threats):
"""Format threats for AI analysis"""
return '\n'.join([
f"- {t['name']} (Severity: {t.get('severity', 'N/A')})"
for t in threats
])This approach gives you full control over the workflow when you need something tmi-mcp does not provide.
- REST-API-Reference - TMI REST API
- Webhook-Integration - Webhook system
- Addon-System - Addon system for extensions
- API-Integration - API integration guide
- Extending-TMI - Extending TMI capabilities