MCP (Model Context Protocol) lets you extend term-llm with external tools: browser automation, database access, API integrations, and more.
# Add from registry
term-llm mcp add playwright # search and install
term-llm mcp add @anthropic/mcp-server-fetch
# Add from URL (HTTP transport)
term-llm mcp add https://developers.openai.com/mcp
# Add a bundled remote server
term-llm mcp add exa # Exa search/fetch MCP
# Use with any command
term-llm exec --mcp playwright "take a screenshot of google.com"
term-llm ask --mcp github "list my open PRs"
term-llm chat --mcp playwright,filesystemMCP Commands#
| Command | Description |
|---|---|
mcp add <name-or-url> |
Add server from registry or URL |
mcp list |
List configured servers |
mcp status [name] |
Show transport and safe authentication metadata |
mcp login <name> |
Sign in to a protected remote server |
mcp logout <name> |
Revoke and remove a stored grant |
mcp info <name> |
Show server info and tools |
mcp run <server> <tool> [args] |
Run MCP tool(s) directly |
mcp remove <name> |
Remove a server |
mcp browse [query] |
Browse/search the MCP registry |
mcp path |
Print config file path |
Adding Servers#
From the registry (stdio transport):
term-llm mcp add playwright # search by name
term-llm mcp add @playwright/mcp # exact package
term-llm mcp browse # interactive browserFrom a URL (HTTP transport):
term-llm mcp add https://developers.openai.com/mcp
term-llm mcp add https://mcp.example.com/apiBundled remote servers:
term-llm mcp add exa # Exa web_search_exa and web_fetch_exa over https://mcp.exa.ai/mcpThis adds Exa’s free remote MCP endpoint. To use your own Exa key with this manually added MCP server, edit mcp.json and add an x-api-key header. The search.exa_mcp.api_key setting applies to term-llm’s built-in search.provider: exa_mcp path.
OAuth sign-in for remote servers#
Streamable HTTP servers can use MCP OAuth automatically. Adding a URL does not contact it or open a browser. Enable the server, then sign in when term-llm reports that authentication is required:
term-llm mcp add https://mcp.example.com/mcp
term-llm mcp login example
term-llm mcp status examplemcp login performs protected-resource and authorization-server discovery, dynamic client registration when needed, PKCE S256, and the browser callback. By default it requests every scope advertised in the authorization server’s scopes_supported metadata. Set oauth.scopes in mcp.json to request a specific subset instead; an explicitly empty list requests only scopes required by the server’s authentication challenge. It prints the authorization URL as a fallback. Use --no-browser when the browser is elsewhere; over SSH, forward the printed loopback callback port (for example with ssh -L) before opening the URL. Device-code authentication is not currently supported.
The resulting registration and grant are stored in $XDG_CONFIG_HOME/term-llm/mcp_oauth.json (normally ~/.config/term-llm/mcp_oauth.json). The directory is mode 0700, the file and lock are mode 0600, writes are atomic, and refresh-token rotation is serialized across term-llm processes. This private file contains credentials: do not copy it into a repository or expose it to a browser. Tokens and client secrets are never returned by the serve API or stored in browser storage.
Use term-llm mcp logout example to attempt RFC 7009 revocation and remove the local grant. --local-only skips the remote attempt. Logout is safe to repeat.
An explicit Authorization header remains authoritative and disables automatic OAuth for that server. Stdio servers continue to use their configured environment. Optional OAuth client configuration belongs in mcp.json, while the secret itself stays in the named environment variable:
{
"servers": {
"private-remote": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"oauth": {
"client_id": "registered-public-client",
"client_secret_env": "MCP_CLIENT_SECRET",
"scopes": ["read", "write"],
"client_id_metadata_url": "https://client.example/metadata.json"
}
}
}
}Set "disabled": true under oauth to opt out without adding a static header.
For web sign-in, term-llm serve derives the callback from the authenticated start request. Set --public-url or TERM_LLM_SERVE_PUBLIC_URL when the browser-visible URL differs. A node mounted behind serve hub must set this to its hub mount, such as https://hub.example/node/<id>, because the hub deliberately strips forwarding headers.
Using MCP Tools#
The --mcp flag works with all commands (ask, exec, edit, chat):
# Single server
term-llm ask --mcp fetch "summarize https://example.com"
term-llm exec --mcp playwright "take a screenshot of google.com"
term-llm edit --mcp github -f main.go "update based on latest API"
# Multiple servers (comma-separated)
term-llm chat --mcp playwright,filesystem,github
# In chat, toggle servers with Ctrl+MRunning Tools Directly#
Use mcp run to call MCP tools without going through the LLM:
# Simple key=value arguments
term-llm mcp run filesystem read_file path=/tmp/test.txt
# JSON arguments for complex values
term-llm mcp run server tool '{"nested":{"deep":"value"}}'
# Multiple tools in one invocation
term-llm mcp run server tool1 key=val tool2 key=val
# Read file contents into a parameter with @path
term-llm mcp run server tool content=@/tmp/big-file.txt
# Read from stdin with @-
cat data.json | term-llm mcp run server tool input=@-Configuration#
MCP servers are stored in ~/.config/term-llm/mcp.json:
{
"servers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp"]
},
"openai-docs": {
"type": "http",
"url": "https://developers.openai.com/mcp"
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxx"
}
},
"authenticated-api": {
"type": "http",
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer your-token"
}
}
}
}Deferred tool discovery#
term-llm keeps small MCP catalogues simple and eagerly sends all schemas. When the complete authorised MCP catalogue exceeds 24 tools, it defers the long tail and lets the model search for the few schemas needed for the task.
Configure the policy in the main config.yaml:
tool_discovery:
mode: auto
strategy: auto
threshold: 24
max_active_tools: 300mode controls whether tools are deferred:
| Mode | Behaviour |
|---|---|
auto |
Eager at or below threshold; deferred above it |
eager |
Always send all authorised MCP schemas |
deferred |
Always defer eligible MCP schemas |
strategy controls how deferred schemas reach the model:
| Strategy | Behaviour |
|---|---|
auto |
Use an exactly supported provider-native path; otherwise use portable search |
portable |
Use term-llm’s ordinary cross-provider tool_search tool |
native |
Require provider-native loading and fail clearly when unsupported |
max_active_tools limits the dynamic provider-visible working set, not the number of tools that may be discovered during a session. It defaults to 300; 0 also selects that default rather than disabling discovery. When the working set is full, term-llm first evicts an unpinned tool that has not yet been sent to the provider, then prefers tools that have never been called before falling back to least-recently-used called tools. Pinned and always_load tools do not consume this dynamic limit and are never evicted. Eviction only changes schema visibility, not authorization: evicted tools remain searchable and can be activated again. If every eligible victim has already been sent, changing the visible schema set may reset provider conversation or prompt-cache state to keep the next request correct.
ChatGPT OAuth gpt-5.6-luna supports native client-executed search. Qwen/Ollama and conventional providers use portable search. Native loading keeps selected schemas in provider discovery output rather than rebuilding the ordinary top-level tool array. Selected MCP tools are grouped under their server namespace on the ChatGPT wire, but discovery, authorization, and execution remain child-granular: selecting one child never loads its siblings. Namespace descriptions use bounded MCP server instructions/metadata when available.
The provider-neutral catalogue still retains each tool’s flattened executable name (server__tool) alongside explicit namespace and child identity. Portable discovery and function-only providers continue using the flattened name, so sessions can move across providers. Native namespace calls are routed through the explicit identity metadata and must match a currently loaded, authorised child; term-llm does not infer authorization by splitting a name or by accepting a namespace alone.
Pin frequent tools so they remain immediately visible when the catalogue is deferred. always_load uses the original server tool name:
{
"servers": {
"github": {
"command": "github-mcp-server",
"always_load": [
"get_pull_request",
"search_issues"
]
}
}
}Discovery changes schema visibility only. MCP server enablement and the engine’s allowed-tools policy remain authoritative for execution.
Transport Types#
| Type | Config | Description |
|---|---|---|
| stdio | command + args |
Runs as subprocess (npm/pypi packages) |
| http | url |
Connects to remote HTTP endpoint |
HTTP transport uses Streamable HTTP (MCP spec 2025-03-26).
Serving tools via MCP#
term-llm serve mcp runs an MCP server over HTTP, exposing term-llm’s tools to any MCP client. This is the inverse of the client workflows above. Instead of consuming external tools, you are publishing your local tools for remote use.
See the dedicated Serving tools via MCP guide for full details, flags, and examples.