Skip to main content
Glama
Rodriguespn

Logflare MCP Code Mode

by Rodriguespn
README.md
# Logflare MCP Code Mode

A [Model Context Protocol](https://modelcontextprotocol.io) server that gives an agent **three tools instead of dozens** for driving the [Logflare](https://logflare.app) Management API. The agent writes a short JavaScript function that runs **inside a [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox)** and calls the API through a thin `api.request()` client — looping, filtering, and chaining calls in one round-trip.

This applies Cloudflare's **[Code Mode](https://blog.cloudflare.com/code-mode-mcp/)** pattern to Logflare:

- LLMs write API-calling code more reliably than long chains of individual tool calls.
- Intermediate data stays in the sandbox, out of the model's context.
- The agent discovers endpoints with `search` (the OpenAPI spec), then calls them with `execute_read`/`execute_write`.

This is a **local stdio server** — one process per MCP client, communicating over stdin/stdout, the same way you'd run any locally-installed MCP server (`npx`, or a `command`/`args` entry in your client config). Logflare has no OAuth 2.1 authorization server, so there's no per-request bearer token to broker the way a remote streamable-HTTP server would; the API key is just an environment variable for the whole process.

---

## Quickstart

**Prerequisites:** Node.js ≥ 20.12, [pnpm](https://pnpm.io) (`corepack enable` provides it), a [Vercel](https://vercel.com) account with Sandbox access, and a Logflare API key (Logflare account → API Keys).

```bash
pnpm install
pnpm build
```

Copy `.env.example` to `.env` and fill in `VERCEL_TOKEN`/`VERCEL_TEAM_ID`/`VERCEL_PROJECT_ID` and `LOGFLARE_API_KEY` — the local `.env` is only for `pnpm dev`/`pnpm test`; a real MCP client sets these as `env` entries in its own config instead (see below).

Connect an MCP client — this repo ships a `.mcp.json`:

```json
{
  "mcpServers": {
    "logflare-code-mode": {
      "command": "node",
      "args": ["dist/index.js"],
      "env": {
        "LOGFLARE_API_KEY": "${LOGFLARE_API_KEY}",
        "VERCEL_TOKEN": "${VERCEL_TOKEN}",
        "VERCEL_TEAM_ID": "${VERCEL_TEAM_ID}",
        "VERCEL_PROJECT_ID": "${VERCEL_PROJECT_ID}"
      }
    }
  }
}
```

Once published, the same thing works via `npx`:

```json
{
  "mcpServers": {
    "logflare-code-mode": {
      "command": "npx",
      "args": ["-y", "logflare-mcp-code-mode", "--read-only"],
      "env": { "LOGFLARE_API_KEY": "...", "VERCEL_TOKEN": "...", "VERCEL_TEAM_ID": "...", "VERCEL_PROJECT_ID": "..." }
    }
  }
}
```

Run `node dist/index.js --help` for the full option/env-var reference.

**Try it** — call `execute_read` with an async arrow function:

```js
async () => {
  const res = await api.request({ method: 'GET', path: '/api/sources' })
  return { status: res.status, count: res.data.length }
}
```

You get back e.g. `{ "status": 200, "count": 3 }` — produced by code the agent wrote, run in a sandbox that can reach only the Logflare API.

---

## The three tools

Each takes a single `code` string — an **async arrow function that returns a value**. `console.log` output comes back separately as `stdout`.

| Tool | What it does | In scope | Annotation |
| --- | --- | --- | --- |
| `search` | Query the Logflare OpenAPI `spec` (all `$ref`s resolved) to find endpoints and their shapes. Network-isolated. | `spec` | read-only |
| `execute_read` | Call the Logflare API via `api.request()` — **GET only**. | `api` | read-only |
| `execute_write` | Call the Logflare API via `api.request()` — any method (POST/PUT/PATCH/DELETE). | `api` | destructive |

The workflow is: `search` to find an endpoint's path and request shape, then `execute_read`/`execute_write` to call it. The client is `api.request({ method, path, query?, body?, contentType?, rawBody? })`, which returns `{ status, ok, data }`.

Run with `--read-only` to lock the whole server instance into read-only mode: `search`'s `spec` only contains GET operations (so the agent can't even discover write endpoints), and `execute_write` isn't registered at all — it's absent from `tools/list`, and calling it anyway fails as an unknown tool. This is a startup flag (a property of the process), not a per-call one.

---

## How it works

```
MCP client ──stdio (newline-delimited JSON-RPC)──▶ McpServer (2 or 3 tools)
                                                          │
                          ┌───────────────────────────────┼───────────────────────────────┐
                        search                       execute_read                   execute_write
                          │                                └───────────────┬───────────────┘
                          ▼                                                ▼
                 fresh Vercel Sandbox                             fresh Vercel Sandbox
                 (networkPolicy: deny-all)                        (networkPolicy: allow logflare.app;
                 embeds `spec`                                     LOGFLARE_API_KEY injected;
                                                                    `api.request()` client)
```

- **Transport** — `StdioServerTransport` from the MCP SDK: reads newline-delimited JSON-RPC from stdin, writes responses to stdout. stdout is exclusively the protocol channel — all logging goes to stderr (see `src/log.ts`).
- **Per call** — a fresh Vercel Sandbox (`@vercel/sandbox`), stopped in `finally`. No SDK is installed inside it, so there's nothing to amortize with a warm pool.
- **`search` sandbox** — `networkPolicy: "deny-all"`; the slimmed OpenAPI spec (fetched from `LOGFLARE_OPENAPI_URL`, default `https://api.logflare.app/api/openapi`) is embedded as `spec` for the agent's read-only code to query. In `--read-only` mode the spec is filtered to GET-only operations first.
- **`execute_*` sandbox** — `networkPolicy: { allow: ["logflare.app"] }` (the Management API's actual host); `LOGFLARE_API_KEY` is injected as an env var and sent as `Authorization: Bearer <key>` by `api.request()`. The read/write split and the credential/backend/team block are enforced by the provided `api.request()` client: `execute_read` throws on any non-GET *before* the fetch, and both tools refuse the paths in `BLOCKED_PATH`. This is a guard against accidental or confused agent behavior, not a hard sandbox capability restriction — agent code is plain Node with the global `fetch` in scope, so it could in principle call `logflare.app` directly and bypass both checks. Safety for a genuinely adversarial caller rests on the fact that they're using their *own* API key against an API they already have full access to, not on this guard.
- **Runner** — agent code is written to a file and run with `node` (never `eval`); output returns via a result file. The code sits on its own lines in the invoke scaffold, so a trailing `// comment` can't swallow the closing tokens.
- **Vercel credentials** — explicit `VERCEL_TOKEN`/`VERCEL_TEAM_ID`/`VERCEL_PROJECT_ID` are required (this process never runs on Vercel itself, so there's no ambient OIDC fallback in practice).

The source is small and layered by responsibility:

| File | Responsibility |
| --- | --- |
| `src/index.ts` | Entry point: parses `--read-only`/`--help`, loads `.env`, starts the stdio transport. |
| `src/server.ts` | The MCP surface: the two or three tools (depending on read-only mode), response formatting, the untrusted-data boundary. |
| `src/sandbox.ts` | Sandbox plumbing: runner construction, the two sandbox shapes, the injected `api.request()` client, output caps, the credential/backend guard. |
| `src/spec.ts` | Fetches, `$ref`-resolves, slims, caches, and (optionally) GET-filters the Logflare OpenAPI spec for `search`. |
| `src/log.ts` | pino logger (stderr only) + credential redaction. |

#### Security notes

Agent code is untrusted but runs inside a Vercel Sandbox microVM and authenticates with the caller's *own* Logflare key — so by design it can use that key against the Logflare API. Beyond the sandbox isolation, per-tool egress, and read/write split above:

- **Untrusted-data boundary** — all execute output (`result`/`stdout`/`stderr`) may embed API content (log events, source metadata), so it's wrapped in a `<untrusted-data-{uuid}>…</>` envelope (error path too), with the model told not to follow instructions inside. Truncation happens before wrapping, so a size cap can't sever the closing tag.
- **Credential/backend/team block** — paths containing `access-tokens` (mints/lists/reveals full API tokens), `backends` (backend `config` can embed DB passwords, service-account keys, and other connection secrets), or `teams` (the Team response embeds the full `User` object, whose `api_key` is the account's master Logflare API key) are refused, matched anywhere in the path (e.g. `/api/sources/{id}/backends/{id}`). The path is first normalized to a fixpoint (percent-decode, re-resolve dot-segments, collapse slashes, lowercase), so encoded spellings (`/%61ccess-tokens`, `//backends`, `/x%2F..%2Fbackends`, `/TEAMS`) still match; it only over-blocks. This guard covers the intended `api.request()` path; see the caveat above about raw `fetch` in the sandbox.
- **Known gap** — some `Source` fields (`slack_hook_url`, `webhook_notification_url`) embed secrets the *caller* configured (their own Slack webhook, etc.) rather than Logflare-minted credentials. These are not blocked, the same way reading your own resource's other fields isn't blocked — but a prompt-injected agent could still surface them into its context via `execute_read`/`execute_write`, same as any other resource field.
- **Output caps** — bounded inside the sandbox before reaching the server: runner result cap (400k chars), `head -c` reads (500k file / 20k per stream), final server truncate (100k result / 10k logs).
- **Token location** — like Vercel Sandbox generally, there's no outbound-fetch proxy hook to keep the token out of the isolate, so `LOGFLARE_API_KEY` lives in the sandbox env (agent code can read it). Safety rests on isolation + egress — the key only reaches `logflare.app` — not token secrecy.
- **Single-tenant by design** — there's no bearer-token auth gate because there's nothing to gate: this process is spawned per-caller by an MCP client, already scoped to whoever's shell/config started it, unlike the remote-HTTP shape this was originally built as (see `git log` — the streamable-HTTP version is preserved in history if a remote deployment is ever needed again).

---

## Configuration

| Env var / flag | Default | Meaning |
| --- | --- | --- |
| `LOGFLARE_API_KEY` (env) | — | **Required.** The Logflare API key used for every `execute_read`/`execute_write` call. |
| `VERCEL_TOKEN` (env) | — | **Required.** Vercel API token for sandbox creation. |
| `VERCEL_TEAM_ID` (env) | — | **Required.** Vercel team ID for sandbox creation. |
| `VERCEL_PROJECT_ID` (env) | — | **Required.** Vercel project ID for sandbox creation. |
| `--read-only` (CLI flag) | off | Hide `execute_write`; restrict `search`'s spec to GET operations. |
| `LOGFLARE_SANDBOX_RUNTIME` (env) | `node24` | Vercel Sandbox runtime the sandboxes boot from |
| `LOGFLARE_EXEC_TIMEOUT_MS` (env) | `120000` | Per-call budget for the agent's code |
| `LOGFLARE_OPENAPI_URL` (env) | `https://api.logflare.app/api/openapi` | Override the OpenAPI spec URL the `search` tool loads |
| `LOG_LEVEL` (env) | `info` | `trace`/`debug`/`info`/`warn`/`error`/`fatal`/`silent` |
| `LOG_FORMAT` (env) | `json` | `json` (structured) or `pretty` (dev) |

---

## Observability

Structured logs via [pino](https://getpino.io) to **stderr only** — stdout is reserved for JSON-RPC. One line per tool invocation/completion (tool name, code length, duration, `calledEndpoints` count for execute calls) plus a startup line noting read-only mode. Use `LOG_FORMAT=pretty` locally. Credentials are never logged (see `redactToken` in `src/log.ts`).

---

## Testing

```bash
pnpm test
```

- **`test/server.test.ts`** — the MCP surface via an in-memory client/server pair (tool list, annotations, error shapes, `readOnly: true` hiding `execute_write`). No key.
- **`test/stdio.test.ts`** — end-to-end over the real stdio transport: spawns the built binary as a child process and speaks newline-delimited JSON-RPC over its stdin/stdout, exactly like a real MCP client. Covers `--help`, `--read-only` over the wire, and that stdout only ever carries JSON-RPC (logs land on stderr). No key. Requires `pnpm build` first.
- **`test/sandbox.test.ts`** — offline unit tests for the credential/backend/team guard (path normalization + block regex). No key, no sandbox.
- **`test/spec.test.ts`** — offline unit tests for the read-only spec filter (GET-only operations, paths with no GET dropped entirely). No key, no network.
- **`test/live.test.ts`** — end-to-end against real Logflare + Vercel Sandbox: spec search, `execute_read` GET plus non-GET rejection, trailing-comment tolerance, error/`SyntaxError` surfacing inside the boundary, the credential/backend/team guard (including encoded bypasses), an `execute_write` create+delete round-trip, egress denial, and the stdout size cap. Runs only when `LOGFLARE_API_KEY` and the `VERCEL_*` credentials are set.

TDQS

A4.6/5.0

Scored across 3 tools

Disambiguation5/5

The three tools have clearly distinct purposes: execute_read for read-only GET requests, execute_write for state-changing requests, and search for exploring the API spec. There is no overlap.

Naming Consistency3/5

The naming pattern is inconsistent: two tools use the 'execute_' prefix (execute_read, execute_write), while the third is simply 'search', breaking the verb_noun pattern.

Tool Count3/5

Three tools is borderline for a server that aims to cover a full API. While the number is not excessive, it feels minimal, and a typical server might include more dedicated tools instead of relying on generic code execution.

Completeness5/5

The tool set covers all possible operations on the Logflare Management API: read, write, and spec exploration. Users can perform any CRUD action via execute_read and execute_write, making the surface complete.

Maintenance

ActivityStale
ResponsivenessNo issues