Files
OwnCord/docs/mcp-introspect.md
T
J3vbandClaude Opus 4.8 58005c9c6f feat(auth): revocable API tokens, introspect MCP server, and a Go 1.26 idiom pass (#1266)
* feat(auth): add revocable API tokens (bot/service auth)

Add long-lived, revocable API tokens so headless clients (the introspection
MCP tool, bots, CI) can authenticate without a password. Presented as
"Authorization: Bearer <token>", a token authenticates as a specific user,
inheriting that user's role and permissions.

- migration 018 + dedicated api_tokens table (kept separate from sessions so
  bulk logout and the per-user session cap never touch these); only the
  SHA-256 hash is stored, raw token shown once at creation
- auth.ResolveTokenHash: one shared bearer resolver that both AuthMiddleware
  and adminAuthMiddleware now call. Sessions are matched first so existing
  login behavior is unchanged; API tokens are a fallback only on session miss.
  A DB outage is returned wrapped, never mistaken for a bad token.
- `server token create|list|revoke` CLI: mints directly against the DB with no
  HTTP and no login — the password-free bootstrap path
- tests: resolver (8 cases incl. outage-not-fallthrough), db queries (6),
  api middleware integration (valid + revoked token)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(tools): add owncord-introspect MCP server

A local MCP dev tool that lets Claude Code introspect a running OwnCord
instance: read its logs, query any REST endpoint, and tail the desktop
client's log file. It is a thin wrapper over the existing API plus the
client log — no new product surface.

- tools/mcp-introspect/index.mjs (Node/ESM, one dep: @modelcontextprotocol/sdk)
  exposes api_request (full read-write passthrough), server_logs (admin SSE
  ring-buffer stream), client_logs (reads the desktop log file)
- authenticates with an API token (OWNCORD_API_TOKEN); pins the self-signed
  cert and skips hostname checks (the cert has no SAN)
- registered in .mcp.json (secret-free ${OWNCORD_API_TOKEN})
- un-ignore tools/mcp-introspect/ so this shared dev tool is committed, while
  tools/livekit-server.exe and node_modules stay ignored
- docs/mcp-introspect.md: how it works, tool reference, setup, troubleshooting

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dependencies): update and add various crate versions in Cargo.lock

* feat(admin): manage API tokens from the admin panel

Add Owner-gated HTTP endpoints and a UI card to create, list, and revoke
API tokens from the web admin panel. Previously only the `server token`
CLI could manage them, which requires shell access to the host.

- POST|GET|DELETE /admin/api/tokens in admin/handlers_tokens.go, wired in
  admin/api.go. All three are Owner-only (ownerOnlyMiddleware, like
  backups/updates): an HTTP token-mint endpoint is a network-reachable
  credential-minting surface, and API tokens deliberately survive password
  change + bulk logout, so a hijacked admin session must not mint one.
- Reuses the same db.*APIToken calls as the CLI; create sources the actor
  from request context (audits who clicked, not the bound user); the raw
  token is returned once in the 201 body, never stored.
- Add json tags to db.APITokenListItem for snake_case wire consistency.
- Admin panel: "API Tokens" nav item + create modal, show-once reveal,
  revoke confirm in admin/static/index.html.
- Tests: 7 in admin/api_test.go (+api_tokens table in the in-memory schema).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: modernize to Go 1.26 idioms + enable modernize linter

Apply `golangci-lint modernize` autofixes across the server and enable the
linter in .golangci.yml so these stop re-accumulating (they built up only
because modernize was never in the config).

Production code: slices.Contains for hand-rolled membership loops (api
router, ws origin, db/account, plugin manifest); strings.SplitSeq for
allocation-free line/segment iteration (db/migrate, updater, livekit_proxy);
strings.Cut (config); fmt.Appendf (dm_handler); min() (event_pruner);
any (ws client). Tests: range-over-int, t.Context(), WaitGroup.Go,
slices.Sort, maps.Copy, new(expr), interface{}->any.

- plugin/manifest.go parent-traversal check applied by hand: modernize
  skipped it (two conflicting rewrites); used the slices.Contains form.
- Removed the now-dead ptr() test helper after newexpr inlined its callers.
- Dropped dangling sort imports left by the sort.Slice->slices.Sort rewrite.

No behavior change. All four tag variants build, full test suite is green,
and golangci-lint (with modernize enabled) reports 0 issues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 13:25:46 +02:00

11 KiB

OwnCord Introspection MCP Server

tools/mcp-introspect/ is a small Model Context Protocol (MCP) server that lets an AI agent — Claude Code — inspect a locally running OwnCord instance: read its logs, query any REST endpoint, and tail the desktop client's log file.

It is a development tool, not part of the shipped product. It ships no data of its own and adds nothing to the server binary — it is a thin wrapper over OwnCord's existing REST API plus the client's on-disk log.

  • Code: tools/mcp-introspect/index.mjs (one file, ~230 lines)
  • Runtime: Node ≥ 20, ESM. One real dependency: @modelcontextprotocol/sdk (+ zod)
  • Registration: /.mcp.json (committed) and .claude/settings.local.json (local)

How it works

flowchart LR
    CC["Claude Code<br/>(MCP client)"] -->|"stdio<br/>JSON-RPC"| MCP["owncord-introspect<br/>index.mjs"]
    MCP -->|"Bearer API token<br/>over pinned TLS"| SRV["OwnCord server<br/>https://127.0.0.1:8443"]
    SRV -->|"REST JSON / SSE"| MCP
    MCP -->|"readFile"| LOG[("owncord-client.log")]

Claude Code launches index.mjs as a child process (node tools/mcp-introspect/index.mjs) and speaks MCP over stdin/stdout. When you (or the agent) call one of its tools, the server makes a request to the local OwnCord instance — or reads a file — and returns the result.

Authentication

OwnCord has no static API key. The only credential its API accepts is a bearer token, and until recently the only way to mint one was a username/password login. This tool instead uses a long-lived API token (added alongside this tool — see API tokens):

  • You mint a token once with server token create (writes directly to the DB, no login).
  • The tool sends it as Authorization: Bearer <token> on every request (OWNCORD_API_TOKEN).
  • Server-side, auth.ResolveTokenHash resolves the bearer token: it checks login sessions first (unchanged behavior), then falls back to API tokens. The token authenticates as the user it was bound to (the owner by default), inheriting that user's role — which is why it can reach /admin/api/* and the log stream.

The tool never sends an Origin header: the server treats a missing Origin as a non-browser client and skips CSRF/Origin checks, so a headless client is never blocked on that axis.

TLS (cert pinning, not CA trust)

OwnCord serves HTTPS with a self-signed certificate that has no SAN (Subject Alternative Name). Trusting it as a CA is not enough — hostname verification against 127.0.0.1 still fails. So the tool uses node:https and:

  1. pins the exact certificate bytes (ca: Server/data/cert.pem), and
  2. skips hostname matching (checkServerIdentity: () => undefined).

Identity is proven by the pin — a MITM would need the identical cert. This is the same trust-on-first-use model the desktop client's proxy already uses. (client_logs needs neither the token nor the cert.)

The three tools

api_request — a single generic passthrough that covers the entire REST API. It issues one https.request to https://127.0.0.1:<port><path> with the bearer token and returns { status, headers, body } (body is JSON-parsed when possible, else raw text). Any HTTP method is allowed, including destructive admin routes.

server_logs — the server keeps its last 2000 log records in an in-memory ring buffer, exposed over Server-Sent Events (an EventSource-style stream can't send an auth header, so it's guarded by a single-use ticket). The tool runs that flow:

sequenceDiagram
    participant T as MCP tool
    participant S as OwnCord server
    T->>S: POST /admin/api/logs/ticket  (Bearer)
    S-->>T: { "ticket": "<hex>" }  (single-use, 30s TTL)
    T->>S: GET /admin/api/logs/stream?ticket=<hex>
    S-->>T: data: {ts,level,msg,source,attrs}  (backfill of ring buffer)
    S-->>T: data: {...}  (live records, if follow_ms > 0)
    T->>T: filter by level/source, apply limit, close

With follow_ms: 0 (default) it returns after the backfill burst goes quiet; with follow_ms > 0 it keeps reading live records for that long. Filtering by level/source and the limit are applied client-side.

client_logs — reads the desktop client's rotating log file directly (%LOCALAPPDATA%\com.owncord.client\logs\owncord-client.log); no server involved. Returns the last N lines with optional level/substring filtering, and degrades gracefully if the file doesn't exist yet.


Setup

1. Install dependencies

cd tools/mcp-introspect
npm install

2. Mint an API token

cd Server
./server.exe token create --label mcp-introspect
# prints the raw token ONCE — copy it now, it is never recoverable

The token defaults to the owner account (so it can reach the admin API and log stream). Bind it to a different user with --user <name>, or set an expiry with --expires 720h. Manage tokens with ./server.exe token list and ./server.exe token revoke <id|label>.

The running server must be built from the current source for API-token auth to work — the feature is compiled into the server binary. Rebuild (go build -o server.exe .) and restart if needed.

3. Put the token in your environment

.mcp.json passes ${OWNCORD_API_TOKEN} through to the tool, so set it once:

setx OWNCORD_API_TOKEN <paste-raw-token>   # Windows user env — open a new shell afterwards

4. Enable in Claude Code

owncord-introspect is already listed in .claude/settings.local.json under enabledMcpjsonServers. Restart Claude Code so it picks up the new MCP server.

Verify

cd tools/mcp-introspect
npx @modelcontextprotocol/inspector node index.mjs

The three tools should appear. Call api_request with { "method": "GET", "path": "/health" } and expect {status:200, body:{...}}.


Tool reference

api_request

Param Type Notes
method string GET, POST, PATCH, PUT, DELETE
path string Path beginning with / (e.g. /admin/api/stats) or a full URL
query object? Query-string params
body any? JSON body (object or string)
headers object? Extra request headers

Returns { status, headers, body }.

// request
{ "method": "GET", "path": "/api/v1/metrics" }
// → { "status": 200, "headers": {...}, "body": { "uptime_seconds": 1820,
//     "goroutines": 42, "connected_users": 1, "voice_sessions": 0, ... } }

Useful read-only endpoints: /health, /api/v1/metrics (runtime/process stats), /admin/api/stats (user/message/channel counts), /api/v1/diagnostics/connectivity, /admin/api/users, /admin/api/channels, /admin/api/audit-log.

server_logs

Param Type Default Notes
level string? DEBUG | INFO | WARN | ERROR
source string? websocket, http, admin, auth, database, storage, updater, config, server
limit number? 500 Max records returned
follow_ms number? 0 0 = backfill only; >0 = also stream live for that many ms

Returns an array of { ts, level, msg, source, attrs } (attrs is parsed from its JSON string when present; req_id/trace_id appear inside attrs).

{ "level": "ERROR", "limit": 50 }              // last 50 ERROR records from the ring buffer
{ "source": "websocket", "follow_ms": 3000 }   // ws logs, backfill + 3s of live tail

client_logs

Param Type Default Notes
lines number? 200 Trailing lines to return
level string? Keep only lines tagged [LEVEL]
grep string? Keep only lines containing this substring

Returns { path, found: true, lines: [...] }, or { path, found: false, note } if the client has not run yet.


Configuration

All optional except the token (which only the two server-backed tools need).

Env var Default Purpose
OWNCORD_API_TOKEN (required for api_request/server_logs) Bearer token from server token create.
OWNCORD_BASE_URL https://127.0.0.1:<server.port> Override the whole base URL (e.g. a non-TLS endpoint). Port is read from Server/config.yaml.
OWNCORD_CERT_PATH Server/data/cert.pem Self-signed cert to pin.
OWNCORD_CLIENT_LOG %LOCALAPPDATA%\com.owncord.client\logs\owncord-client.log Desktop client log path.

Troubleshooting

Symptom Cause / fix
OWNCORD_API_TOKEN is not set Mint a token and set the env var; restart the shell/Claude Code so it's inherited.
OwnCord cert not found at … Start the server once to generate Server/data/cert.pem, or set OWNCORD_CERT_PATH / OWNCORD_BASE_URL.
api_request returns 401 Token missing/revoked/expired, or (for /admin/*) the token's user lacks ADMINISTRATOR. Mint a fresh owner-bound token.
api_request returns 403 on /admin/* The request didn't come from an allowed IP — the tool must run on the same host as the server (localhost is allowed by default).
server_logs fails at the ticket step The token can't reach /admin/api/* (needs ADMINISTRATOR), or the server isn't the current build.
client_logsfound: false The desktop client hasn't run yet, or the path differs — set OWNCORD_CLIENT_LOG.

Security notes

  • Local-only by design. The tool talks to 127.0.0.1 and relies on the server's localhost admin IP gate. Do not expose it or point it at a remote host without understanding the trust model.
  • api_request is full read-write. It can call any endpoint with any method, including destructive admin routes (delete users/channels, restore backups, apply updates). There is no write allowlist — it relies on the operator/agent's discretion.
  • The API token is a real credential. It never expires by default and inherits the owner's permissions. Keep it out of version control (it lives in your environment, not .mcp.json), and revoke it with server token revoke if leaked.

API tokens (server side)

The MCP tool depends on a server feature added at the same time: revocable, long-lived API tokens. See the api_tokens table in schema.md and the server token subcommands. Key points:

  • Stored hashed (SHA-256), like sessions; the raw token is shown once at creation.
  • Resolved by the same middleware as sessions (auth.ResolveTokenHash) — sessions are matched first, so existing login behavior is unchanged; API tokens are a fallback.
  • expires_at IS NULL means never expires; revoked_at IS NULL means active. Revocation takes effect immediately.
  • Kept in a separate table from sessions, so bulk logout and the per-user session cap never affect them.