MCP Integration - ericfitz/tmi GitHub Wiki

MCP Integration

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.

How It Works

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.

Install

macOS, with Homebrew (signed and notarized binary):

brew install ericfitz/tap/tmi-mcp
tmi-mcp init

Elsewhere, with Go:

go install github.com/ericfitz/tmi-mcp/cmd/tmi-mcp@latest
tmi-mcp init

tmi-mcp version prints the installed version. Upgrade a Homebrew install with brew upgrade tmi-mcp.

What tmi-mcp init does

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|grok configures only that harness, even if it is not detected.
  • --dry-run prints 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 local

Configuration

tmi-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.

Server Requirement: OAuth Callback Allowlist

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.

Tools

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.

Authentication Behavior

  • 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-mcp if you run several agent sessions against the same profile.

TMI's Own AI Assistant

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.

Learn More About MCP

The Model Context Protocol is an open standard for connecting AI assistants to data sources and tools.

Resources:

Alternative Integrations

Besides tmi-mcp, you can integrate AI tools with TMI directly using:

  1. REST API: Use TMI's REST API to build custom AI integrations (example: the TMI Terraform analyzer, see TMI-Terraform-Analyzer)

  2. Webhooks: Receive real-time notifications for AI processing

  3. Addons: Build AI-powered addons using the addon system

Example: AI Integration via API

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.

Related Documentation

⚠️ **GitHub.com Fallback** ⚠️