Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 80 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 1Password Device Trust (Kolide K2) MCP Server

An MCP (Model Context Protocol) server that exposes the 1Password Device Trust API (formerly Kolide K2) as tools for AI agents. Uses Streamable HTTP transport for communication.
An MCP (Model Context Protocol) server that exposes the 1Password Device Trust API (formerly Kolide K2) as tools for AI agents. Supports both Streamable HTTP and stdio transports.

## Features

Expand All @@ -14,7 +14,7 @@ An MCP (Model Context Protocol) server that exposes the 1Password Device Trust A
- **Bearer token authentication** on all MCP endpoints
- **Structured JSON audit logging** of every tool invocation
- Binds to localhost only by default; configurable CORS allowlist
- Streamable HTTP transport for easy integration with AI tools
- **Dual transport** — Streamable HTTP for long-running shared servers, or stdio for zero-setup subprocess launches by Claude Code, Claude Desktop, and other stdio-capable clients
- API key and Kolide API version (`KOLIDE_API_VERSION`) read fresh on each request (supports `.env` updates without restart)

## Maintaining API parity
Expand Down Expand Up @@ -85,22 +85,29 @@ cp .env.example .env
| `MCP_PORT` | `8000` | Listen port |
| `MCP_CORS_ALLOWED_ORIGINS` | `http://localhost,http://127.0.0.1` | Comma-separated origins for browser-based MCP clients |
| `MCP_MAX_ENRICH_RECORDS` | `500` | Max records enriched per `enrich_device_owner` call |
| `MCP_LOG_FILE` | *(unset)* | File path for structured audit logs (in addition to stdout) |
| `MCP_LOG_FILE` | *(unset)* | File path for structured audit logs (in addition to stderr) |
| `MCP_DEBUG` | `false` | Starlette debug mode (development only) |

The Kolide API key and API version header are read fresh on each tool call (using your `.env` if present), so you can change `KOLIDE_API_KEY` or `KOLIDE_API_VERSION` without restarting the server.

## Running the Server

### Using uv
The server supports two transports. Both expose the same tools, resources, and behavior — they only differ in how the MCP client reaches the server.

```bash
uv run kolide-mcp
```
### Which mode should I use?

| Mode | When to use it |
|---|---|
| **HTTP** (`kolide-mcp`) | You want one long-running server shared by multiple clients (Cursor, VS Code, browser-based agents), need to run the server on a different machine, or want a process you can monitor with the `/health` endpoint. Requires `MCP_AUTH_TOKEN` for bearer-token auth. |
| **stdio** (`kolide-mcp-stdio`) | You want zero setup: the MCP client launches the server as a subprocess on demand, with no port to manage, no auth token, and no need for the `mcp-remote` bridge. Works out of the box with Claude Code, Claude Desktop, and any stdio-capable MCP client. Lifetime is tied to the client. |

You can run both at the same time — they're independent processes — so HTTP clients and stdio clients can share the same install.

### Using Python directly
### HTTP mode

```bash
uv run kolide-mcp
# or
python -m kolide_mcp.server
```

Expand All @@ -111,7 +118,17 @@ MCP endpoint: http://127.0.0.1:8000/mcp
Health check: http://127.0.0.1:8000/health
```

> **Note:** The server refuses to start if `MCP_AUTH_TOKEN` is not set.
> **Note:** HTTP mode refuses to start if `MCP_AUTH_TOKEN` is not set.

### stdio mode

You normally don't run this command yourself — your MCP client launches it. To smoke-test:

```bash
uv run kolide-mcp-stdio
```

The process reads JSON-RPC from stdin and writes responses to stdout; logs go to stderr. `MCP_AUTH_TOKEN` is **not** required (stdio inherits the parent process's trust boundary), but `KOLIDE_API_KEY` still is.

## Connecting AI Tools

Expand Down Expand Up @@ -159,6 +176,38 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
}
```

### Claude Code

Claude Code supports stdio servers directly. Register the server with the `claude mcp add` CLI (recommended) or by editing your MCP config file by hand.

Using the CLI:

```bash
claude mcp add kolide -- uv --directory /absolute/path/to/device-trust-mcp-server run kolide-mcp-stdio
```

Or add to your Claude Code MCP config (`~/.claude.json` user-scope, or `.mcp.json` in your project root):

```json
{
"mcpServers": {
"kolide": {
"command": "uv",
"args": [
"--directory", "/absolute/path/to/device-trust-mcp-server",
"run", "kolide-mcp-stdio"
],
"env": {
"KOLIDE_API_KEY": "your-kolide-api-key",
"KOLIDE_API_VERSION": "2026-04-07"
}
}
}
}
```

If you'd rather point Claude Code at a running HTTP server, use the same `mcp-remote` pattern shown in the Claude Desktop section above.

### VS Code (Copilot Chat)

Requires VS Code 1.99+ with MCP support. Add to `.vscode/mcp.json` in your project, then open the Copilot Chat panel in **Agent** mode:
Expand All @@ -179,7 +228,28 @@ Requires VS Code 1.99+ with MCP support. Add to `.vscode/mcp.json` in your proje

### Other MCP Clients

Connect to the MCP endpoint at `http://localhost:8000/mcp` with an `Authorization: Bearer <token>` header. The server uses the Streamable HTTP transport. Clients that only support stdio can use `mcp-remote` as shown in the Claude Desktop example above.
**HTTP transport** — Connect to `http://localhost:8000/mcp` with an `Authorization: Bearer <token>` header. The server uses the Streamable HTTP transport.

**stdio transport** — If your client supports stdio subprocess servers (most modern MCP clients do), launch `kolide-mcp-stdio` directly and skip the running server entirely:

```json
{
"mcpServers": {
"kolide": {
"command": "uv",
"args": [
"--directory", "/absolute/path/to/device-trust-mcp-server",
"run", "kolide-mcp-stdio"
],
"env": {
"KOLIDE_API_KEY": "your-kolide-api-key"
}
}
}
}
```

The client manages the process lifetime; no port, no auth token, and no `mcp-remote` bridge are needed. Clients that only support HTTP can use `mcp-remote` as shown in the Claude Desktop example above.

Replace `YOUR_MCP_AUTH_TOKEN` in all examples with the same value you set in `MCP_AUTH_TOKEN`.

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ test = [

[project.scripts]
kolide-mcp = "kolide_mcp.server:main"
kolide-mcp-stdio = "kolide_mcp.server:main_stdio"

[build-system]
requires = ["hatchling"]
Expand Down
11 changes: 6 additions & 5 deletions src/kolide_mcp/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@ def format(self, record: logging.LogRecord) -> str:
def setup_logging(log_file: str | None = None) -> logging.Logger:
"""Configure and return the kolide_mcp logger.

Logs are written to stdout. If log_file is provided, logs are also
written to that file path. Both handlers use structured JSON format.
Logs are written to stderr (so they don't corrupt the JSON-RPC channel
when running under stdio transport). If log_file is provided, logs are
also written to that file path. Both handlers use structured JSON format.
"""
logger = logging.getLogger("kolide_mcp")
logger.setLevel(logging.INFO)
logger.propagate = False

formatter = StructuredFormatter()

stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter)
logger.addHandler(stdout_handler)
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setFormatter(formatter)
logger.addHandler(stderr_handler)

if log_file:
file_handler = logging.FileHandler(log_file)
Expand Down
50 changes: 50 additions & 0 deletions src/kolide_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,56 @@ async def dispatch(self, request, call_next):
return app


def main_stdio():
"""Run the MCP server over stdio (subprocess transport).

Unlike the HTTP entry point, this skips MCP_AUTH_TOKEN: stdio inherits the
parent process's trust boundary, so the bearer token would be meaningless.
KOLIDE_API_KEY is still required (validated on first tool call).
"""
import asyncio
import sys

from mcp.server.stdio import stdio_server

setup_logging(config.log_file)

try:
get_kolide_api_version()
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
sys.exit(1)

async def _run():
Comment thread
JohnEchevesteMM marked this conversation as resolved.
_request_ip.set("stdio")
registry = create_registry(client)
try:
await registry.load()
except Exception:
logger.warning(
"Failed to load reporting tables at startup; will retry on first use"
)
try:
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options(),
)
finally:
await client.close()

try:
asyncio.run(_run())
except KeyboardInterrupt:
# asyncio.run() has already propagated cancellation through _run(), so
# client.close() and stdio_server teardown ran. Bypass interpreter
# finalization because anyio's stdin reader is a daemon thread blocked
# in a C-level read() that threading._shutdown can't join.
import os
os._exit(0)


def main():
"""Run the MCP server with Streamable HTTP transport."""
import sys
Expand Down
Loading