lattice-mcp 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +226 -0
- package/README.md +205 -0
- package/index.js +820 -1
- package/package.json +1 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# AGENTS.md — lattice-mcp
|
|
2
|
+
|
|
3
|
+
> `lattice-mcp` is the **Model Context Protocol server for Lattice**, the container
|
|
4
|
+
> orchestration platform that runs every `appleby.cloud` service. It exposes the
|
|
5
|
+
> `lattice-api` admin surface to Claude Code as **125 typed tools** — workers, stacks,
|
|
6
|
+
> containers, deployments, databases, registries, networks, volumes and instance config.
|
|
7
|
+
> This file orients any agent/worker before touching code in this repo.
|
|
8
|
+
>
|
|
9
|
+
> **⚠️ Golden rule — keep this file current:** any change that adds, removes or retypes a
|
|
10
|
+
> tool, changes the auth model, or drifts from `lattice-api`'s route surface MUST update this
|
|
11
|
+
> AGENTS.md in the SAME change. Stale context here misleads every future agent. If you finish
|
|
12
|
+
> work and haven't touched AGENTS.md, confirm that's actually correct.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What this repo is
|
|
17
|
+
|
|
18
|
+
A single-file Node ESM program (`index.js`, ~1,180 lines) that speaks MCP over stdio and
|
|
19
|
+
translates tool calls into HTTP requests against `lattice-api`. It is published to npm as
|
|
20
|
+
`lattice-mcp` and consumed via `npx -y lattice-mcp` from `~/.mcp.json`.
|
|
21
|
+
|
|
22
|
+
It owns **only the translation layer**: tool names, argument schemas, descriptions, and URL
|
|
23
|
+
construction. It holds no business logic, no caching, and no state. Every behaviour an agent
|
|
24
|
+
observes — pagination limits, validation messages, side effects — comes from `lattice-api`.
|
|
25
|
+
|
|
26
|
+
It does **not** own: the Lattice data model, deployment mechanics, or the worker protocol.
|
|
27
|
+
Those live in [`lattice-api`](https://github.com/aidenappl/lattice-api) and
|
|
28
|
+
[`lattice-runner`](https://github.com/aidenappl/lattice-runner).
|
|
29
|
+
|
|
30
|
+
## Stack & dependencies
|
|
31
|
+
|
|
32
|
+
- **Runtime:** Node ≥18 (needs global `fetch` and `AbortSignal.timeout`). `"type": "module"` —
|
|
33
|
+
ESM only, top-level `await` is used at the bottom of `index.js`.
|
|
34
|
+
- **`@modelcontextprotocol/sdk` ^1.29.0** — `McpServer` + `StdioServerTransport`.
|
|
35
|
+
- **`zod`** — argument schemas. Supplied transitively by the MCP SDK; it is *not* a declared
|
|
36
|
+
dependency, which is a latent fragility (see Rules & guardrails).
|
|
37
|
+
- No build step, no bundler, no tests, no lint config. `node --check index.js` is the only
|
|
38
|
+
static gate.
|
|
39
|
+
|
|
40
|
+
## Project structure
|
|
41
|
+
|
|
42
|
+
| Path | Role |
|
|
43
|
+
|------|------|
|
|
44
|
+
| `index.js` | Everything: `--setup` flow, config read, `api()` HTTP helper, `text()`/`body()` helpers, all 125 `server.tool(...)` registrations, transport connect. |
|
|
45
|
+
| `package.json` | npm metadata. `bin.lattice-mcp` → `index.js`, so `npx lattice-mcp` works. |
|
|
46
|
+
| `README.md` | User-facing setup + full tool table. |
|
|
47
|
+
| `AGENTS.md` | This file. |
|
|
48
|
+
|
|
49
|
+
`index.js` is organised top-to-bottom as: setup block → config/guard → helpers → tools grouped
|
|
50
|
+
by domain under `// ───` banner comments → transport. **Keep new tools inside the matching
|
|
51
|
+
banner group**; do not append to the bottom.
|
|
52
|
+
|
|
53
|
+
## Running, building & testing
|
|
54
|
+
|
|
55
|
+
There is no `Devfile.yaml` and no `dev` CLI wiring here — it is a single script.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node --check index.js # syntax gate — the only static check that exists
|
|
59
|
+
npm install # needed before running locally (deps are not vendored)
|
|
60
|
+
node index.js --setup # interactive: writes the lattice block into ~/.mcp.json
|
|
61
|
+
LATTICE_API_URL=... LATTICE_API_TOKEN=... node index.js # run the server on stdio
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Smoke-testing without an MCP client.** The server speaks JSON-RPC over stdio, so you can
|
|
65
|
+
drive it with a shell pipeline. This is the standard way to verify a change registers cleanly:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
{ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}'
|
|
69
|
+
sleep 2
|
|
70
|
+
echo '{"jsonrpc":"2.0","method":"notifications/initialized"}'
|
|
71
|
+
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
|
|
72
|
+
sleep 2; } | LATTICE_API_URL=x LATTICE_API_TOKEN=x node index.js
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The `sleep`s matter — without them the requests race the handshake and you get nothing back.
|
|
76
|
+
For a live call, swap `tools/list` for
|
|
77
|
+
`{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"lattice_get_anomalies","arguments":{}}}`
|
|
78
|
+
and supply the real token.
|
|
79
|
+
|
|
80
|
+
## How code is written here
|
|
81
|
+
|
|
82
|
+
- **Every tool follows one shape.** Deviating makes the file harder to scan:
|
|
83
|
+
```js
|
|
84
|
+
server.tool("lattice_<verb>_<noun>", "<description>", { /* zod schema */ }, async (args) => {
|
|
85
|
+
const res = await api("<METHOD>", `/admin/<path>`, params, body);
|
|
86
|
+
return { content: text(res) };
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
- **Naming:** `lattice_` prefix on every tool, then `list|get|create|update|delete|<verb>` then
|
|
90
|
+
the noun (`lattice_list_database_instances`). The prefix is what disambiguates these from
|
|
91
|
+
`monitor_*` and `forta_*` tools in a shared client.
|
|
92
|
+
- **`api(method, path, params, body)`** — `params` become query string entries (undefined/null
|
|
93
|
+
are dropped), `body` is JSON-encoded. Errors are swallowed and returned as
|
|
94
|
+
`{success:false, error_message}` so a network failure surfaces as tool output rather than a
|
|
95
|
+
transport crash.
|
|
96
|
+
- **`body(obj)`** strips `undefined` keys. **Always use it on PUT/PATCH tools.** The API treats
|
|
97
|
+
a present-but-null field as an explicit clear, so forwarding raw `{...fields}` on an update
|
|
98
|
+
would wipe every field the caller didn't pass.
|
|
99
|
+
- **Path params are template literals**, query params go through the `params` argument. Never
|
|
100
|
+
hand-concatenate a query string.
|
|
101
|
+
- **Descriptions are the contract.** An agent picks tools from the description alone, so it
|
|
102
|
+
must state what the tool does, when to reach for it, and — for anything destructive — the
|
|
103
|
+
blast radius. Compare `lattice_stop_container` ("stops it") with `lattice_delete_container`
|
|
104
|
+
("Destructive — lattice_stop_container only stops it").
|
|
105
|
+
- **Read the handler before adding a tool.** Struct field names are not the request contract;
|
|
106
|
+
handlers frequently override or ignore them. Two live examples from `lattice-api`:
|
|
107
|
+
`HandleUpdateCompose` has a `containerConfigFingerprint` helper with short JSON keys
|
|
108
|
+
(`i`, `t`, `pm`) that is **not** the request body — the body is just `{compose_yaml}`; and
|
|
109
|
+
`HandleDatabaseAction` derives its action from the **last URL path segment**, not from a body
|
|
110
|
+
field.
|
|
111
|
+
|
|
112
|
+
## Domain & architecture
|
|
113
|
+
|
|
114
|
+
**Auth.** A single long-lived bearer token (`LATTICE_API_TOKEN`) from
|
|
115
|
+
`lattice-api`'s `/admin/api-tokens`, sent on every request. There is no refresh, no expiry
|
|
116
|
+
handling, and no login flow. A 401 means the token was revoked or expired — the fix is a new
|
|
117
|
+
token via `--setup`, not a code change.
|
|
118
|
+
|
|
119
|
+
**Config.** Read once at startup from `LATTICE_API_URL` and `LATTICE_API_TOKEN`; the process
|
|
120
|
+
exits immediately if either is missing. In practice these come from the `env` block of the
|
|
121
|
+
`lattice` entry in `~/.mcp.json`.
|
|
122
|
+
|
|
123
|
+
**Tool groups**, in file order:
|
|
124
|
+
|
|
125
|
+
| Group | Tools | Notes |
|
|
126
|
+
|-------|-------|-------|
|
|
127
|
+
| Overview & health | 2 | `lattice_overview`, `lattice_health` |
|
|
128
|
+
| Workers | 3 + 4 actions | list/get/metrics; reboot, upgrade, stop-all, start-all |
|
|
129
|
+
| Stacks | 2 + 4 actions | list/get; deploy, restart, stop, start, update |
|
|
130
|
+
| Containers | 5 + 8 actions | get/list/logs/lifecycle/metrics; start, stop, restart, kill, pause, unpause, remove, recreate |
|
|
131
|
+
| Deployments | 4 | list/get/logs, rollback, approve |
|
|
132
|
+
| Audit & API tokens | 4 | audit log; token list/create/delete |
|
|
133
|
+
| **Database instances** | **11** | CRUD, `lattice_database_action`, credentials, snapshots, restore |
|
|
134
|
+
| **Backup destinations** | **6** | CRUD + test |
|
|
135
|
+
| **Registries** | **8** | CRUD, test, test-inline, repositories, tags |
|
|
136
|
+
| **Discovery & diagnostics** | **7** | search, anomalies, fleet-metrics, versions, refresh-versions, container metrics, self |
|
|
137
|
+
| **Stacks — compose & tokens** | **13** | create/delete, compose update/sync/import, export/import, save-template, deploy tokens |
|
|
138
|
+
| **Containers — definition CRUD** | **3** | create/update/delete definitions |
|
|
139
|
+
| **Workers — resources** | **16** | worker CRUD, tokens, volumes, networks, force-remove |
|
|
140
|
+
| **Global env vars / templates / webhooks** | **12** | |
|
|
141
|
+
| **Users & instance config** | **12** | users, SSO, SMTP, notification prefs |
|
|
142
|
+
|
|
143
|
+
Bolded groups were added in **1.1.0**, closing a gap where the MCP had drifted roughly two
|
|
144
|
+
months behind `lattice-api` — database instances shipped in the API in May 2026 and were
|
|
145
|
+
entirely unreachable from the MCP until then.
|
|
146
|
+
|
|
147
|
+
**Consolidations.** Where `lattice-api` exposes several paths served by one handler, this repo
|
|
148
|
+
exposes one tool with an enum rather than N tools. `lattice_database_action` covers
|
|
149
|
+
`/start`, `/stop`, `/restart` and `/remove`. Worker actions are the historical exception — they
|
|
150
|
+
predate this convention and remain separate tools.
|
|
151
|
+
|
|
152
|
+
## Ecosystem & related repos
|
|
153
|
+
|
|
154
|
+
| Repo | Relationship |
|
|
155
|
+
|------|--------------|
|
|
156
|
+
| [`lattice-api`](https://github.com/aidenappl/lattice-api) | The API this wraps. Its `main.go` route table is the source of truth for coverage. |
|
|
157
|
+
| [`lattice-web`](https://github.com/aidenappl/lattice-web) | Next.js dashboard over the same API. |
|
|
158
|
+
| [`lattice-runner`](https://github.com/aidenappl/lattice-runner) | Agent on each worker VM; WebSocket back to `lattice-api`. |
|
|
159
|
+
| [`monitor-mcp`](https://github.com/aidenappl/monitor-mcp) | Sibling MCP, same single-file structure — keep them stylistically aligned. |
|
|
160
|
+
| `forta-mcp` / `keyring-mcp` / `openbucket-mcp` | Newer siblings; they carry a `body()` helper and destructive-blast-radius descriptions that originated here. |
|
|
161
|
+
|
|
162
|
+
## Operations
|
|
163
|
+
|
|
164
|
+
- **Published to npm** as `lattice-mcp` (public). Consumers run `npx -y lattice-mcp`, which
|
|
165
|
+
resolves the latest published version — so **publishing is deployment**. A bug shipped to npm
|
|
166
|
+
reaches every user on their next MCP server start.
|
|
167
|
+
- **Publishing requires 2FA via passkey.** `npm publish` must run from an interactive terminal:
|
|
168
|
+
npm's web auth flow needs to open a browser, and from a non-TTY subprocess it degrades to
|
|
169
|
+
demanding an OTP that a passkey-only account cannot produce.
|
|
170
|
+
- **In-session staleness:** a running MCP server process does not pick up a new npm version.
|
|
171
|
+
After publishing, the client must be restarted before the new tools/schemas appear.
|
|
172
|
+
- **Common failure modes:**
|
|
173
|
+
- *All tools return `API request failed: fetch failed`* — `LATTICE_API_URL` is wrong or the
|
|
174
|
+
TLS proxy in front of Lattice has an expired cert.
|
|
175
|
+
- *All tools return 401* — token revoked or expired; re-run `--setup`.
|
|
176
|
+
- *One tool 404s while others work* — the MCP is ahead of the deployed `lattice-api`, or the
|
|
177
|
+
route moved.
|
|
178
|
+
|
|
179
|
+
## Rules & guardrails
|
|
180
|
+
|
|
181
|
+
- **Never hardcode a token, URL or hostname.** Everything comes from env.
|
|
182
|
+
- **Never log request or response bodies.** Responses routinely contain env vars, registry
|
|
183
|
+
credentials and database passwords. `lattice_get_database_credentials` returns live secrets
|
|
184
|
+
by design — do not add convenience logging anywhere in `api()`.
|
|
185
|
+
- **Never add a tool without reading its handler in `lattice-api`.** Inferring a request shape
|
|
186
|
+
from a struct has produced real, shipped bugs across this family of servers.
|
|
187
|
+
- **Do not break tool names.** They are a public contract: renaming one silently breaks any
|
|
188
|
+
saved workflow or prompt that referenced it. Add a new tool and deprecate in the description
|
|
189
|
+
instead.
|
|
190
|
+
- **`zod` is used but not declared** in `package.json` — it resolves transitively through the
|
|
191
|
+
MCP SDK. If the SDK ever drops or hoists it differently, every tool schema breaks at startup.
|
|
192
|
+
Adding it as an explicit dependency is the correct fix; do it in a standalone change.
|
|
193
|
+
- **Keep destructive descriptions honest.** If a tool destroys data, the description must say so
|
|
194
|
+
and name the safer alternative.
|
|
195
|
+
- Publishing is outward-facing and effectively irreversible (npm unpublish is restricted after
|
|
196
|
+
72 hours) — do not publish without explicit instruction.
|
|
197
|
+
|
|
198
|
+
## Verification — always before "done"
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
node --check index.js # must pass
|
|
202
|
+
grep -c 'server.tool(' index.js # tool count matches what you expect
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Then the stdio handshake from *Running, building & testing* above, asserting:
|
|
206
|
+
- the server registers **without stderr output**,
|
|
207
|
+
- the tool count is what you expect,
|
|
208
|
+
- **no duplicate tool names** (`server.tool` silently accepts a duplicate; the last registration
|
|
209
|
+
wins and the earlier tool disappears — this will not error).
|
|
210
|
+
|
|
211
|
+
For any tool you added or changed, make **one real call against the live API** and confirm the
|
|
212
|
+
response shape. Schema-only verification is not enough: it catches typos, not wrong units,
|
|
213
|
+
wrong enum values, or parameters the handler ignores.
|
|
214
|
+
|
|
215
|
+
**Never report work complete on the strength of `tools/list` alone.**
|
|
216
|
+
|
|
217
|
+
## Keeping this file updated
|
|
218
|
+
|
|
219
|
+
Update this AGENTS.md in the same change when you:
|
|
220
|
+
- **Add/remove/rename a tool** → update the tool-group table and the count in the header.
|
|
221
|
+
- **Change the auth model or config vars** → update *Domain & architecture*.
|
|
222
|
+
- **Change the `api()`/`body()`/`text()` helpers** → update *How code is written here*.
|
|
223
|
+
- **Bump the version or publish** → note behavioural changes under the relevant group.
|
|
224
|
+
- **Notice `lattice-api` has gained routes** → either add the tools or record the gap here
|
|
225
|
+
explicitly, so the next agent knows it was a decision and not an oversight.
|
|
226
|
+
- Also keep `README.md`'s tool tables in sync — it is the user-facing surface and drifts fastest.
|
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# lattice-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for the [Lattice](https://github.com/aidenappl/lattice-api) container orchestration platform. Gives Claude Code direct access to manage workers, stacks, containers, and deployments.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx lattice-mcp --setup
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This prompts for your Lattice API URL and API token, writes the config to `~/.mcp.json`, and you're ready to go. Restart Claude Code after setup.
|
|
12
|
+
|
|
13
|
+
## Manual Setup
|
|
14
|
+
|
|
15
|
+
Add to `~/.mcp.json`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"lattice": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "lattice-mcp"],
|
|
23
|
+
"env": {
|
|
24
|
+
"LATTICE_API_URL": "https://lattice-api.appleby.cloud",
|
|
25
|
+
"LATTICE_API_TOKEN": "your-api-token"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Generate an API token from the Lattice web dashboard under **Settings > API Tokens**.
|
|
33
|
+
|
|
34
|
+
## Tools
|
|
35
|
+
|
|
36
|
+
### Overview & Health
|
|
37
|
+
| Tool | Description |
|
|
38
|
+
|------|-------------|
|
|
39
|
+
| `lattice_overview` | Fleet overview — worker counts, stack counts, failed stacks, CPU/memory |
|
|
40
|
+
| `lattice_health` | API health and database connectivity |
|
|
41
|
+
|
|
42
|
+
### Workers
|
|
43
|
+
| Tool | Description |
|
|
44
|
+
|------|-------------|
|
|
45
|
+
| `lattice_list_workers` | List workers with status, IP, versions |
|
|
46
|
+
| `lattice_get_worker` | Detailed worker info |
|
|
47
|
+
| `lattice_get_worker_metrics` | CPU, memory, disk, network metrics |
|
|
48
|
+
| `lattice_reboot_worker` | Reboot a worker machine |
|
|
49
|
+
| `lattice_upgrade_worker` | Upgrade worker runner to latest |
|
|
50
|
+
| `lattice_stop_all_worker` | Stop all containers on a worker |
|
|
51
|
+
| `lattice_start_all_worker` | Start all containers on a worker |
|
|
52
|
+
|
|
53
|
+
### Stacks
|
|
54
|
+
| Tool | Description |
|
|
55
|
+
|------|-------------|
|
|
56
|
+
| `lattice_list_stacks` | List stacks with status and worker assignment |
|
|
57
|
+
| `lattice_get_stack` | Full stack details including compose YAML |
|
|
58
|
+
| `lattice_update_stack` | Update stack configuration |
|
|
59
|
+
| `lattice_deploy_stack` | Deploy a stack (all or specific containers) |
|
|
60
|
+
| `lattice_restart_stack` | Restart all containers in a stack |
|
|
61
|
+
| `lattice_stop_stack` | Stop all containers in a stack |
|
|
62
|
+
| `lattice_start_stack` | Start all containers in a stack |
|
|
63
|
+
|
|
64
|
+
### Containers
|
|
65
|
+
| Tool | Description |
|
|
66
|
+
|------|-------------|
|
|
67
|
+
| `lattice_list_containers` | List containers with status, image, ports, health |
|
|
68
|
+
| `lattice_get_container` | Full container details |
|
|
69
|
+
| `lattice_get_container_logs` | Recent container logs (stdout/stderr) |
|
|
70
|
+
| `lattice_get_container_lifecycle` | Lifecycle events (start, stop, health changes) |
|
|
71
|
+
| `lattice_start_container` | Start a stopped container |
|
|
72
|
+
| `lattice_stop_container` | Stop a running container |
|
|
73
|
+
| `lattice_restart_container` | Restart a container |
|
|
74
|
+
| `lattice_kill_container` | Force kill a container |
|
|
75
|
+
| `lattice_pause_container` | Pause a running container |
|
|
76
|
+
| `lattice_unpause_container` | Unpause a paused container |
|
|
77
|
+
| `lattice_remove_container` | Remove a container |
|
|
78
|
+
| `lattice_recreate_container` | Remove and recreate a container |
|
|
79
|
+
|
|
80
|
+
### Deployments
|
|
81
|
+
| Tool | Description |
|
|
82
|
+
|------|-------------|
|
|
83
|
+
| `lattice_list_deployments` | List deployments with status and timing |
|
|
84
|
+
| `lattice_get_deployment` | Deployment details with container-level status |
|
|
85
|
+
| `lattice_get_deployment_logs` | Pull, create, start, swap events with timing |
|
|
86
|
+
| `lattice_rollback_deployment` | Rollback to previous state |
|
|
87
|
+
|
|
88
|
+
### System
|
|
89
|
+
| Tool | Description |
|
|
90
|
+
|------|-------------|
|
|
91
|
+
| `lattice_get_audit_log` | Recent audit log entries |
|
|
92
|
+
| `lattice_update_api` | Trigger API self-update |
|
|
93
|
+
| `lattice_update_web` | Trigger web container update |
|
|
94
|
+
| `lattice_list_api_tokens` | List API tokens |
|
|
95
|
+
| `lattice_create_api_token` | Create a new API token |
|
|
96
|
+
| `lattice_delete_api_token` | Delete an API token |
|
|
97
|
+
|
|
98
|
+
## Example Prompts
|
|
99
|
+
|
|
100
|
+
- "What's the status of all stacks?"
|
|
101
|
+
- "Show me logs for the forta-api container"
|
|
102
|
+
- "Deploy stack 5"
|
|
103
|
+
- "Which containers are unhealthy?"
|
|
104
|
+
- "Rollback the last deployment on stack 12"
|
|
105
|
+
|
|
106
|
+
## Environment Variables
|
|
107
|
+
|
|
108
|
+
| Variable | Required | Description |
|
|
109
|
+
|----------|----------|-------------|
|
|
110
|
+
| `LATTICE_API_URL` | Yes | Lattice API base URL |
|
|
111
|
+
| `LATTICE_API_TOKEN` | Yes | API token for authentication |
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
116
|
+
|
|
117
|
+
### Database instances
|
|
118
|
+
| Tool | Description |
|
|
119
|
+
|------|-------------|
|
|
120
|
+
| `lattice_list_database_instances` | List managed databases (filter by worker, engine, status) |
|
|
121
|
+
| `lattice_get_database_instance` | Full instance config |
|
|
122
|
+
| `lattice_create_database_instance` | Provision mysql/mariadb/postgres on a worker |
|
|
123
|
+
| `lattice_update_database_instance` | Update config, limits, snapshot schedule |
|
|
124
|
+
| `lattice_delete_database_instance` | Delete an instance ⚠️ |
|
|
125
|
+
| `lattice_database_action` | start / stop / restart / remove ⚠️ |
|
|
126
|
+
| `lattice_get_database_credentials` | Connection credentials (returns secrets) |
|
|
127
|
+
| `lattice_list_database_snapshots` | Snapshots for an instance |
|
|
128
|
+
| `lattice_create_database_snapshot` | Take a snapshot now |
|
|
129
|
+
| `lattice_restore_database_snapshot` | Restore from a snapshot ⚠️ |
|
|
130
|
+
| `lattice_delete_database_snapshot` | Delete a snapshot ⚠️ |
|
|
131
|
+
|
|
132
|
+
### Backup destinations
|
|
133
|
+
| Tool | Description |
|
|
134
|
+
|------|-------------|
|
|
135
|
+
| `lattice_list_backup_destinations` / `lattice_get_backup_destination` | Inventory |
|
|
136
|
+
| `lattice_create_backup_destination` / `lattice_update_backup_destination` | Manage destinations |
|
|
137
|
+
| `lattice_delete_backup_destination` | Delete ⚠️ |
|
|
138
|
+
| `lattice_test_backup_destination` | Test connectivity without writing a backup |
|
|
139
|
+
|
|
140
|
+
### Registries
|
|
141
|
+
| Tool | Description |
|
|
142
|
+
|------|-------------|
|
|
143
|
+
| `lattice_list_registries` | Configured registries |
|
|
144
|
+
| `lattice_create_registry` / `lattice_update_registry` | Manage registries |
|
|
145
|
+
| `lattice_delete_registry` | Delete ⚠️ |
|
|
146
|
+
| `lattice_test_registry` / `lattice_test_registry_inline` | Test stored or unsaved credentials |
|
|
147
|
+
| `lattice_list_registry_repositories` | What images exist |
|
|
148
|
+
| `lattice_list_registry_tags` | **What versions are deployable** |
|
|
149
|
+
|
|
150
|
+
### Discovery & diagnostics
|
|
151
|
+
| Tool | Description |
|
|
152
|
+
|------|-------------|
|
|
153
|
+
| `lattice_search` | Search workers, stacks and containers in one call |
|
|
154
|
+
| `lattice_get_anomalies` | **Restart loops, unhealthy containers, offline workers — best first call** |
|
|
155
|
+
| `lattice_get_fleet_metrics` | Aggregated fleet CPU/memory/disk/network |
|
|
156
|
+
| `lattice_get_versions` / `lattice_refresh_versions` | Runner versions and what's outdated |
|
|
157
|
+
| `lattice_get_container_metrics` | Per-container metrics over time |
|
|
158
|
+
| `lattice_get_self` | Which user the token authenticates as |
|
|
159
|
+
|
|
160
|
+
### Stacks — compose, export & deploy tokens
|
|
161
|
+
| Tool | Description |
|
|
162
|
+
|------|-------------|
|
|
163
|
+
| `lattice_create_stack` / `lattice_delete_stack` | Stack lifecycle ⚠️ |
|
|
164
|
+
| `lattice_get_stack_containers` | Containers in a stack |
|
|
165
|
+
| `lattice_update_stack_compose` / `lattice_sync_stack_compose` | Compose YAML management |
|
|
166
|
+
| `lattice_import_compose` | Create a stack from compose YAML |
|
|
167
|
+
| `lattice_export_stack` / `lattice_import_stack_export` | Portable stack backup/restore |
|
|
168
|
+
| `lattice_save_stack_as_template` | Save a stack as a reusable template |
|
|
169
|
+
| `lattice_list_deploy_tokens` | CI deploy tokens — `last_used_at` shows whether CI reaches Lattice |
|
|
170
|
+
| `lattice_create_deploy_token` / `lattice_delete_deploy_token` | Manage CI deploy tokens ⚠️ |
|
|
171
|
+
| `lattice_approve_deployment` | Approve a deployment awaiting approval |
|
|
172
|
+
|
|
173
|
+
### Container definitions
|
|
174
|
+
| Tool | Description |
|
|
175
|
+
|------|-------------|
|
|
176
|
+
| `lattice_create_container` / `lattice_update_container` | Manage container definitions |
|
|
177
|
+
| `lattice_delete_container` | Delete definition and container ⚠️ |
|
|
178
|
+
|
|
179
|
+
### Workers — resources
|
|
180
|
+
| Tool | Description |
|
|
181
|
+
|------|-------------|
|
|
182
|
+
| `lattice_create_worker` / `lattice_update_worker` / `lattice_delete_worker` | Worker registration ⚠️ |
|
|
183
|
+
| `lattice_get_worker_container_stats` | Live per-container stats |
|
|
184
|
+
| `lattice_list_worker_tokens` / `lattice_create_worker_token` / `lattice_delete_worker_token` | Runner registration tokens ⚠️ |
|
|
185
|
+
| `lattice_list_worker_volumes` / `lattice_create_worker_volume` / `lattice_delete_worker_volume` | Docker volumes ⚠️ |
|
|
186
|
+
| `lattice_list_worker_networks` / `lattice_create_worker_network` / `lattice_delete_worker_network` | Docker networks ⚠️ |
|
|
187
|
+
| `lattice_list_all_networks` / `lattice_delete_network` | Fleet-wide networks ⚠️ |
|
|
188
|
+
| `lattice_force_remove_container` | Force-remove a wedged container ⚠️ |
|
|
189
|
+
|
|
190
|
+
### Env vars, templates & webhooks
|
|
191
|
+
| Tool | Description |
|
|
192
|
+
|------|-------------|
|
|
193
|
+
| `lattice_list_env_vars` / `lattice_create_env_var` / `lattice_update_env_var` / `lattice_delete_env_var` | Global `${VAR}` interpolation values ⚠️ |
|
|
194
|
+
| `lattice_list_templates` / `lattice_create_template` / `lattice_delete_template` | Stack templates ⚠️ |
|
|
195
|
+
| `lattice_list_webhooks` / `lattice_create_webhook` / `lattice_update_webhook` / `lattice_delete_webhook` / `lattice_test_webhook` | Outbound event webhooks ⚠️ |
|
|
196
|
+
|
|
197
|
+
### Users & instance configuration
|
|
198
|
+
| Tool | Description |
|
|
199
|
+
|------|-------------|
|
|
200
|
+
| `lattice_list_users` / `lattice_create_user` / `lattice_update_user` / `lattice_delete_user` | User management ⚠️ |
|
|
201
|
+
| `lattice_get_sso_config` / `lattice_update_sso_config` | Forta SSO ⚠️ |
|
|
202
|
+
| `lattice_get_smtp_config` / `lattice_update_smtp_config` / `lattice_test_smtp` | Alert email |
|
|
203
|
+
| `lattice_get_notification_prefs` / `lattice_update_notification_prefs` | Per-event notification prefs |
|
|
204
|
+
|
|
205
|
+
⚠️ = destructive. Tool descriptions state the blast radius.
|
package/index.js
CHANGED
|
@@ -2,12 +2,58 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { createInterface } from "readline";
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
|
|
10
|
+
// --- Interactive setup ---
|
|
11
|
+
|
|
12
|
+
if (process.argv.includes("--setup")) {
|
|
13
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
15
|
+
|
|
16
|
+
console.log("\n Lattice MCP Setup\n");
|
|
17
|
+
|
|
18
|
+
const apiUrl = (await ask(" Lattice API URL (https://lattice-api.appleby.cloud): ")).trim() || "https://lattice-api.appleby.cloud";
|
|
19
|
+
const apiToken = (await ask(" Lattice API Token: ")).trim();
|
|
20
|
+
rl.close();
|
|
21
|
+
|
|
22
|
+
if (!apiToken) {
|
|
23
|
+
console.error("\n Error: API token is required.\n");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const mcpPath = join(homedir(), ".mcp.json");
|
|
28
|
+
let config = { mcpServers: {} };
|
|
29
|
+
if (existsSync(mcpPath)) {
|
|
30
|
+
try { config = JSON.parse(readFileSync(mcpPath, "utf-8")); } catch {}
|
|
31
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
config.mcpServers.lattice = {
|
|
35
|
+
command: "npx",
|
|
36
|
+
args: ["-y", "lattice-mcp"],
|
|
37
|
+
env: {
|
|
38
|
+
LATTICE_API_URL: apiUrl,
|
|
39
|
+
LATTICE_API_TOKEN: apiToken,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
44
|
+
console.log(`\n Written to ${mcpPath}`);
|
|
45
|
+
console.log(" Restart Claude Code to load the Lattice MCP server.\n");
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- MCP Server ---
|
|
5
50
|
|
|
6
51
|
const API_URL = process.env.LATTICE_API_URL;
|
|
7
52
|
const API_TOKEN = process.env.LATTICE_API_TOKEN;
|
|
8
53
|
|
|
9
54
|
if (!API_URL || !API_TOKEN) {
|
|
10
|
-
console.error("LATTICE_API_URL and LATTICE_API_TOKEN are required");
|
|
55
|
+
console.error("LATTICE_API_URL and LATTICE_API_TOKEN are required.");
|
|
56
|
+
console.error("Run `npx lattice-mcp --setup` to configure.");
|
|
11
57
|
process.exit(1);
|
|
12
58
|
}
|
|
13
59
|
|
|
@@ -43,6 +89,12 @@ function text(data) {
|
|
|
43
89
|
return [{ type: "text", text: JSON.stringify(data, null, 2) }];
|
|
44
90
|
}
|
|
45
91
|
|
|
92
|
+
// Strips undefined keys so a PUT only sends the fields the caller supplied —
|
|
93
|
+
// the API treats a present-but-null field as an explicit clear.
|
|
94
|
+
function body(obj) {
|
|
95
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
96
|
+
}
|
|
97
|
+
|
|
46
98
|
// --- MCP Server ---
|
|
47
99
|
|
|
48
100
|
const server = new McpServer({
|
|
@@ -351,6 +403,773 @@ server.tool("lattice_delete_api_token", "Delete an API token", {
|
|
|
351
403
|
return { content: text(res) };
|
|
352
404
|
});
|
|
353
405
|
|
|
406
|
+
|
|
407
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
408
|
+
// Database instances
|
|
409
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
server.tool("lattice_list_database_instances", "List managed database instances with engine, version, status, worker and health. Filter by worker, engine or status", {
|
|
412
|
+
worker_id: z.number().optional().describe("Filter by worker ID"),
|
|
413
|
+
engine: z.enum(["mysql", "mariadb", "postgres"]).optional().describe("Filter by engine"),
|
|
414
|
+
status: z.string().optional().describe("Filter by status (running, stopped, creating, error)"),
|
|
415
|
+
limit: z.number().optional().describe("Max instances to return"),
|
|
416
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
417
|
+
}, async (args) => {
|
|
418
|
+
const res = await api("GET", "/admin/database-instances", args);
|
|
419
|
+
return { content: text(res) };
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
server.tool("lattice_get_database_instance", "Get one database instance: engine, port, limits, snapshot schedule, retention and backup destination", {
|
|
423
|
+
id: z.number().describe("Database instance ID"),
|
|
424
|
+
}, async ({ id }) => {
|
|
425
|
+
const res = await api("GET", `/admin/database-instances/${id}`);
|
|
426
|
+
return { content: text(res) };
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
server.tool("lattice_create_database_instance", "Provision a database instance on a worker. Creates a real container — pick a port that is free on that worker", {
|
|
430
|
+
name: z.string().describe("Instance name (must be unique)"),
|
|
431
|
+
engine: z.enum(["mysql", "mariadb", "postgres"]).describe("Database engine"),
|
|
432
|
+
worker_id: z.number().describe("Worker to provision on"),
|
|
433
|
+
engine_version: z.string().optional().describe("Engine version tag; the API picks a default when omitted"),
|
|
434
|
+
port: z.number().optional().describe("Host port to expose"),
|
|
435
|
+
root_password: z.string().optional().describe("Root/superuser password"),
|
|
436
|
+
database_name: z.string().optional().describe("Initial database to create"),
|
|
437
|
+
username: z.string().optional().describe("Application user to create"),
|
|
438
|
+
password: z.string().optional().describe("Application user's password"),
|
|
439
|
+
cpu_limit: z.number().optional().describe("CPU limit in cores"),
|
|
440
|
+
memory_limit: z.number().optional().describe("Memory limit in bytes"),
|
|
441
|
+
snapshot_schedule: z.string().optional().describe("Cron expression for automatic snapshots"),
|
|
442
|
+
retention_count: z.number().optional().describe("How many automatic snapshots to keep"),
|
|
443
|
+
backup_destination_id: z.number().optional().describe("Backup destination for snapshots"),
|
|
444
|
+
}, async (args) => {
|
|
445
|
+
const res = await api("POST", "/admin/database-instances", null, body(args));
|
|
446
|
+
return { content: text(res) };
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
server.tool("lattice_update_database_instance", "Update a database instance's configuration. Only the fields you pass are changed", {
|
|
450
|
+
id: z.number().describe("Database instance ID"),
|
|
451
|
+
name: z.string().optional(),
|
|
452
|
+
status: z.string().optional(),
|
|
453
|
+
port: z.number().optional(),
|
|
454
|
+
root_password: z.string().optional().describe("New root password"),
|
|
455
|
+
password: z.string().optional().describe("New application user password"),
|
|
456
|
+
cpu_limit: z.number().optional(),
|
|
457
|
+
memory_limit: z.number().optional(),
|
|
458
|
+
health_status: z.string().optional(),
|
|
459
|
+
snapshot_schedule: z.string().optional().describe("Cron expression for automatic snapshots"),
|
|
460
|
+
retention_count: z.number().optional(),
|
|
461
|
+
backup_destination_id: z.number().optional(),
|
|
462
|
+
active: z.boolean().optional(),
|
|
463
|
+
}, async ({ id, ...fields }) => {
|
|
464
|
+
const res = await api("PUT", `/admin/database-instances/${id}`, null, body(fields));
|
|
465
|
+
return { content: text(res) };
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
server.tool("lattice_delete_database_instance", "Delete a database instance record. Destructive — data is lost unless a snapshot exists. Check lattice_list_database_snapshots first", {
|
|
469
|
+
id: z.number().describe("Database instance ID"),
|
|
470
|
+
}, async ({ id }) => {
|
|
471
|
+
const res = await api("DELETE", `/admin/database-instances/${id}`);
|
|
472
|
+
return { content: text(res) };
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
server.tool("lattice_database_action", "Start, stop, restart or remove a database instance's container. 'remove' destroys the container — data survives only if the volume or a snapshot does", {
|
|
476
|
+
id: z.number().describe("Database instance ID"),
|
|
477
|
+
action: z.enum(["start", "stop", "restart", "remove"]).describe("Action to perform"),
|
|
478
|
+
}, async ({ id, action }) => {
|
|
479
|
+
const res = await api("POST", `/admin/database-instances/${id}/${action}`);
|
|
480
|
+
return { content: text(res) };
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
server.tool("lattice_get_database_credentials", "Get a database instance's connection credentials. Returns secret values — avoid unless the credentials are actually needed", {
|
|
484
|
+
id: z.number().describe("Database instance ID"),
|
|
485
|
+
}, async ({ id }) => {
|
|
486
|
+
const res = await api("GET", `/admin/database-instances/${id}/credentials`);
|
|
487
|
+
return { content: text(res) };
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
server.tool("lattice_list_database_snapshots", "List snapshots for a database instance, with size and creation time. Check this before deleting or restoring an instance", {
|
|
491
|
+
id: z.number().describe("Database instance ID"),
|
|
492
|
+
}, async ({ id }) => {
|
|
493
|
+
const res = await api("GET", `/admin/database-instances/${id}/snapshots`);
|
|
494
|
+
return { content: text(res) };
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
server.tool("lattice_create_database_snapshot", "Take a snapshot of a database instance now, outside its schedule", {
|
|
498
|
+
id: z.number().describe("Database instance ID"),
|
|
499
|
+
}, async ({ id }) => {
|
|
500
|
+
const res = await api("POST", `/admin/database-instances/${id}/snapshots`);
|
|
501
|
+
return { content: text(res) };
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
server.tool("lattice_restore_database_snapshot", "Restore a database instance from one of its snapshots. Overwrites current data irreversibly — take a fresh snapshot first if the present state matters", {
|
|
505
|
+
id: z.number().describe("Database instance ID"),
|
|
506
|
+
snapshot_id: z.number().describe("Snapshot ID to restore from (see lattice_list_database_snapshots)"),
|
|
507
|
+
}, async ({ id, snapshot_id }) => {
|
|
508
|
+
const res = await api("POST", `/admin/database-instances/${id}/restore`, null, { snapshot_id });
|
|
509
|
+
return { content: text(res) };
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
server.tool("lattice_delete_database_snapshot", "Delete a database snapshot. Destructive — that restore point is gone", {
|
|
513
|
+
snapshot_id: z.number().describe("Snapshot ID"),
|
|
514
|
+
}, async ({ snapshot_id }) => {
|
|
515
|
+
const res = await api("DELETE", `/admin/database-snapshots/${snapshot_id}`);
|
|
516
|
+
return { content: text(res) };
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
520
|
+
// Backup destinations
|
|
521
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
522
|
+
|
|
523
|
+
server.tool("lattice_list_backup_destinations", "List configured backup destinations that database snapshots can be shipped to", {}, async () => {
|
|
524
|
+
const res = await api("GET", "/admin/backup-destinations");
|
|
525
|
+
return { content: text(res) };
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
server.tool("lattice_get_backup_destination", "Get one backup destination's configuration", {
|
|
529
|
+
id: z.number().describe("Backup destination ID"),
|
|
530
|
+
}, async ({ id }) => {
|
|
531
|
+
const res = await api("GET", `/admin/backup-destinations/${id}`);
|
|
532
|
+
return { content: text(res) };
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
server.tool("lattice_create_backup_destination", "Create a backup destination. config is a free-form object whose shape depends on type (e.g. an S3 destination takes bucket, region, endpoint and credentials)", {
|
|
536
|
+
name: z.string().describe("Destination name"),
|
|
537
|
+
type: z.string().describe("Destination type, e.g. 's3'"),
|
|
538
|
+
config: z.record(z.any()).describe("Type-specific configuration object"),
|
|
539
|
+
}, async (args) => {
|
|
540
|
+
const res = await api("POST", "/admin/backup-destinations", null, args);
|
|
541
|
+
return { content: text(res) };
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
server.tool("lattice_update_backup_destination", "Update a backup destination. Passing config replaces the whole object, it does not merge", {
|
|
545
|
+
id: z.number().describe("Backup destination ID"),
|
|
546
|
+
name: z.string().optional(),
|
|
547
|
+
type: z.string().optional(),
|
|
548
|
+
config: z.record(z.any()).optional().describe("Replacement configuration object"),
|
|
549
|
+
active: z.boolean().optional(),
|
|
550
|
+
}, async ({ id, ...fields }) => {
|
|
551
|
+
const res = await api("PUT", `/admin/backup-destinations/${id}`, null, body(fields));
|
|
552
|
+
return { content: text(res) };
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
server.tool("lattice_delete_backup_destination", "Delete a backup destination. Instances pointing at it lose their backup target. Destructive", {
|
|
556
|
+
id: z.number().describe("Backup destination ID"),
|
|
557
|
+
}, async ({ id }) => {
|
|
558
|
+
const res = await api("DELETE", `/admin/backup-destinations/${id}`);
|
|
559
|
+
return { content: text(res) };
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
server.tool("lattice_test_backup_destination", "Test connectivity and credentials for a backup destination without writing a real backup", {
|
|
563
|
+
id: z.number().describe("Backup destination ID"),
|
|
564
|
+
worker_id: z.number().optional().describe("Worker to run the test from"),
|
|
565
|
+
}, async ({ id, worker_id }) => {
|
|
566
|
+
const res = await api("POST", `/admin/backup-destinations/${id}/test`, { worker_id });
|
|
567
|
+
return { content: text(res) };
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
571
|
+
// Registries
|
|
572
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
573
|
+
|
|
574
|
+
server.tool("lattice_list_registries", "List configured container registries", {}, async () => {
|
|
575
|
+
const res = await api("GET", "/admin/registries");
|
|
576
|
+
return { content: text(res) };
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
server.tool("lattice_create_registry", "Add a container registry", {
|
|
580
|
+
name: z.string().describe("Registry name"),
|
|
581
|
+
url: z.string().describe("Registry URL, e.g. registry.appleby.cloud"),
|
|
582
|
+
type: z.string().describe("Registry type, e.g. 'generic', 'dockerhub', 'ghcr'"),
|
|
583
|
+
username: z.string().optional().describe("Registry username"),
|
|
584
|
+
password: z.string().optional().describe("Registry password or access token"),
|
|
585
|
+
}, async (args) => {
|
|
586
|
+
const res = await api("POST", "/admin/registries", null, body(args));
|
|
587
|
+
return { content: text(res) };
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
server.tool("lattice_update_registry", "Update a registry's URL, type, credentials or active flag", {
|
|
591
|
+
id: z.number().describe("Registry ID"),
|
|
592
|
+
name: z.string().optional(),
|
|
593
|
+
url: z.string().optional(),
|
|
594
|
+
type: z.string().optional(),
|
|
595
|
+
username: z.string().optional(),
|
|
596
|
+
password: z.string().optional(),
|
|
597
|
+
active: z.boolean().optional(),
|
|
598
|
+
}, async ({ id, ...fields }) => {
|
|
599
|
+
const res = await api("PUT", `/admin/registries/${id}`, null, body(fields));
|
|
600
|
+
return { content: text(res) };
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
server.tool("lattice_delete_registry", "Delete a registry. Containers pulling from it fail on their next deploy. Destructive", {
|
|
604
|
+
id: z.number().describe("Registry ID"),
|
|
605
|
+
}, async ({ id }) => {
|
|
606
|
+
const res = await api("DELETE", `/admin/registries/${id}`);
|
|
607
|
+
return { content: text(res) };
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
server.tool("lattice_test_registry", "Test a saved registry's stored credentials", {
|
|
611
|
+
id: z.number().describe("Registry ID"),
|
|
612
|
+
}, async ({ id }) => {
|
|
613
|
+
const res = await api("POST", `/admin/registries/${id}/test`);
|
|
614
|
+
return { content: text(res) };
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
server.tool("lattice_test_registry_inline", "Test registry credentials before saving them", {
|
|
618
|
+
url: z.string().describe("Registry URL"),
|
|
619
|
+
username: z.string().describe("Registry username"),
|
|
620
|
+
password: z.string().describe("Registry password or access token"),
|
|
621
|
+
}, async (args) => {
|
|
622
|
+
const res = await api("POST", "/admin/registries/test", null, args);
|
|
623
|
+
return { content: text(res) };
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
server.tool("lattice_list_registry_repositories", "List repositories available in a registry — what images exist to deploy", {
|
|
627
|
+
id: z.number().describe("Registry ID"),
|
|
628
|
+
}, async ({ id }) => {
|
|
629
|
+
const res = await api("GET", `/admin/registries/${id}/repositories`);
|
|
630
|
+
return { content: text(res) };
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
server.tool("lattice_list_registry_tags", "List the tags published for one repository. Use this to answer 'what version can I deploy' or 'is the image my CI just built actually in the registry'", {
|
|
634
|
+
id: z.number().describe("Registry ID"),
|
|
635
|
+
repo: z.string().describe("Repository name, e.g. 'openbucket-api'"),
|
|
636
|
+
}, async ({ id, repo }) => {
|
|
637
|
+
const res = await api("GET", `/admin/registries/${id}/tags`, { repo });
|
|
638
|
+
return { content: text(res) };
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
642
|
+
// Discovery & diagnostics
|
|
643
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
644
|
+
|
|
645
|
+
server.tool("lattice_search", "Search across workers, stacks and containers by name in one call. Faster than listing each type and filtering when you only have a partial name", {
|
|
646
|
+
q: z.string().describe("Search query"),
|
|
647
|
+
limit: z.number().optional().describe("Max results"),
|
|
648
|
+
}, async ({ q, limit }) => {
|
|
649
|
+
const res = await api("GET", "/admin/search", { q, limit });
|
|
650
|
+
return { content: text(res) };
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
server.tool("lattice_get_anomalies", "Get detected fleet anomalies — restart loops, unhealthy containers, resource spikes, offline workers. The best first call when asked whether anything is wrong", {}, async () => {
|
|
654
|
+
const res = await api("GET", "/admin/anomalies");
|
|
655
|
+
return { content: text(res) };
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
server.tool("lattice_get_fleet_metrics", "Get aggregated fleet-wide CPU, memory, disk and network metrics over a time range", {
|
|
659
|
+
range: z.string().optional().describe("Time range, e.g. '1h', '6h', '24h'"),
|
|
660
|
+
}, async ({ range }) => {
|
|
661
|
+
const res = await api("GET", "/admin/fleet-metrics", { range });
|
|
662
|
+
return { content: text(res) };
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
server.tool("lattice_get_versions", "Get runner versions across workers and which are outdated. Use before lattice_upgrade_worker to see what actually needs upgrading", {}, async () => {
|
|
666
|
+
const res = await api("GET", "/admin/versions");
|
|
667
|
+
return { content: text(res) };
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
server.tool("lattice_refresh_versions", "Re-poll every worker for its current runner version, refreshing what lattice_get_versions reports", {}, async () => {
|
|
671
|
+
const res = await api("POST", "/admin/versions/refresh");
|
|
672
|
+
return { content: text(res) };
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
server.tool("lattice_get_container_metrics", "Get a single container's CPU, memory and network metrics over time", {
|
|
676
|
+
id: z.number().describe("Container ID"),
|
|
677
|
+
limit: z.number().optional().describe("Max data points"),
|
|
678
|
+
range: z.string().optional().describe("Time range, e.g. '1h', '6h', '24h'"),
|
|
679
|
+
}, async ({ id, limit, range }) => {
|
|
680
|
+
const res = await api("GET", `/admin/containers/${id}/metrics`, { limit, range });
|
|
681
|
+
return { content: text(res) };
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
server.tool("lattice_get_self", "Get the user this API token authenticates as, including role", {}, async () => {
|
|
685
|
+
const res = await api("GET", "/admin/self");
|
|
686
|
+
return { content: text(res) };
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
690
|
+
// Stacks — lifecycle, compose and deploy tokens
|
|
691
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
692
|
+
|
|
693
|
+
server.tool("lattice_create_stack", "Create an empty stack. Add containers with lattice_create_container, or use lattice_import_compose to create one from compose YAML", {
|
|
694
|
+
name: z.string().describe("Stack name"),
|
|
695
|
+
description: z.string().optional().describe("Description"),
|
|
696
|
+
worker_id: z.number().optional().describe("Worker to pin the stack to"),
|
|
697
|
+
deployment_strategy: z.string().optional().describe("Deployment strategy, e.g. 'rolling', 'recreate'"),
|
|
698
|
+
auto_deploy: z.boolean().optional().describe("Redeploy automatically when a new image is pushed"),
|
|
699
|
+
env_vars: z.string().optional().describe("Stack-level env vars as a JSON object string"),
|
|
700
|
+
}, async (args) => {
|
|
701
|
+
const res = await api("POST", "/admin/stacks", null, body(args));
|
|
702
|
+
return { content: text(res) };
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
server.tool("lattice_delete_stack", "Delete a stack and every container in it. Destructive and not recoverable — export it first with lattice_export_stack if you might want it back", {
|
|
706
|
+
id: z.number().describe("Stack ID"),
|
|
707
|
+
}, async ({ id }) => {
|
|
708
|
+
const res = await api("DELETE", `/admin/stacks/${id}`);
|
|
709
|
+
return { content: text(res) };
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
server.tool("lattice_get_stack_containers", "List the containers belonging to one stack", {
|
|
713
|
+
id: z.number().describe("Stack ID"),
|
|
714
|
+
}, async ({ id }) => {
|
|
715
|
+
const res = await api("GET", `/admin/stacks/${id}/containers`);
|
|
716
|
+
return { content: text(res) };
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
server.tool("lattice_update_stack_compose", "Replace a stack's compose YAML. This rewrites the stored definition but does not deploy — call lattice_deploy_stack afterwards", {
|
|
720
|
+
id: z.number().describe("Stack ID"),
|
|
721
|
+
compose_yaml: z.string().describe("Full compose YAML document"),
|
|
722
|
+
}, async ({ id, compose_yaml }) => {
|
|
723
|
+
const res = await api("PUT", `/admin/stacks/${id}/compose`, null, { compose_yaml });
|
|
724
|
+
return { content: text(res) };
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
server.tool("lattice_sync_stack_compose", "Reconcile a stack's container records against its stored compose YAML, reporting which containers changed and why", {
|
|
728
|
+
id: z.number().describe("Stack ID"),
|
|
729
|
+
}, async ({ id }) => {
|
|
730
|
+
const res = await api("POST", `/admin/stacks/${id}/sync-compose`);
|
|
731
|
+
return { content: text(res) };
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
server.tool("lattice_import_compose", "Create a new stack from a docker-compose YAML document", {
|
|
735
|
+
name: z.string().describe("Stack name"),
|
|
736
|
+
compose_yaml: z.string().describe("Full compose YAML document"),
|
|
737
|
+
description: z.string().optional().describe("Description"),
|
|
738
|
+
worker_id: z.number().optional().describe("Worker to pin the stack to"),
|
|
739
|
+
deployment_strategy: z.string().optional().describe("Deployment strategy"),
|
|
740
|
+
}, async (args) => {
|
|
741
|
+
const res = await api("POST", "/admin/stacks/import", null, body(args));
|
|
742
|
+
return { content: text(res) };
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
server.tool("lattice_export_stack", "Export a stack's full definition — stack settings plus every container config — as a portable JSON document. Take one before any destructive stack change", {
|
|
746
|
+
id: z.number().describe("Stack ID"),
|
|
747
|
+
}, async ({ id }) => {
|
|
748
|
+
const res = await api("GET", `/admin/stacks/${id}/export`);
|
|
749
|
+
return { content: text(res) };
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
server.tool("lattice_import_stack_export", "Recreate a stack from a document produced by lattice_export_stack", {
|
|
753
|
+
version: z.string().describe("Export format version, from the exported document"),
|
|
754
|
+
stack: z.record(z.any()).describe("Stack object from the exported document"),
|
|
755
|
+
containers: z.array(z.record(z.any())).describe("Container array from the exported document"),
|
|
756
|
+
}, async (args) => {
|
|
757
|
+
const res = await api("POST", "/admin/stacks/import-export", null, args);
|
|
758
|
+
return { content: text(res) };
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
server.tool("lattice_save_stack_as_template", "Save an existing stack's configuration as a reusable template", {
|
|
762
|
+
id: z.number().describe("Stack ID to snapshot"),
|
|
763
|
+
name: z.string().describe("Template name"),
|
|
764
|
+
description: z.string().optional().describe("Description"),
|
|
765
|
+
}, async ({ id, name, description }) => {
|
|
766
|
+
const res = await api("POST", `/admin/stacks/${id}/save-template`, null, body({ name, description }));
|
|
767
|
+
return { content: text(res) };
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
server.tool("lattice_list_deploy_tokens", "List a stack's deploy tokens — the credentials CI uses to trigger deployments. Token values are never returned; last_used_at reveals whether CI is actually reaching Lattice", {
|
|
771
|
+
id: z.number().describe("Stack ID"),
|
|
772
|
+
}, async ({ id }) => {
|
|
773
|
+
const res = await api("GET", `/admin/stacks/${id}/deploy-tokens`);
|
|
774
|
+
return { content: text(res) };
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
server.tool("lattice_create_deploy_token", "Create a deploy token for a stack. The plaintext is returned once; use it as https://<lattice>/api/deploy/<token>?container=<name>", {
|
|
778
|
+
id: z.number().describe("Stack ID"),
|
|
779
|
+
name: z.string().describe("Token name, e.g. 'github-actions'"),
|
|
780
|
+
}, async ({ id, name }) => {
|
|
781
|
+
const res = await api("POST", `/admin/stacks/${id}/deploy-tokens`, null, { name });
|
|
782
|
+
return { content: text(res) };
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
server.tool("lattice_delete_deploy_token", "Delete a deploy token. Any CI pipeline using it stops being able to deploy. Destructive", {
|
|
786
|
+
token_id: z.number().describe("Deploy token ID"),
|
|
787
|
+
}, async ({ token_id }) => {
|
|
788
|
+
const res = await api("DELETE", `/admin/deploy-tokens/${token_id}`);
|
|
789
|
+
return { content: text(res) };
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
server.tool("lattice_approve_deployment", "Approve a deployment that is waiting on manual approval", {
|
|
793
|
+
id: z.number().describe("Deployment ID"),
|
|
794
|
+
}, async ({ id }) => {
|
|
795
|
+
const res = await api("POST", `/admin/deployments/${id}/approve`);
|
|
796
|
+
return { content: text(res) };
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
800
|
+
// Containers — definition CRUD
|
|
801
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
802
|
+
|
|
803
|
+
server.tool("lattice_create_container", "Add a container definition to a stack. Creates the record only — deploy the stack to actually start it. JSON-shaped fields are passed as strings, matching the API", {
|
|
804
|
+
stack_id: z.number().describe("Stack to add the container to"),
|
|
805
|
+
name: z.string().describe("Container name"),
|
|
806
|
+
image: z.string().describe("Image, e.g. registry.appleby.cloud/openbucket-api"),
|
|
807
|
+
tag: z.string().describe("Image tag, e.g. 'latest'"),
|
|
808
|
+
port_mappings: z.string().optional().describe("JSON array string, e.g. '[{\"container_port\":\"8000\",\"host_port\":\"8080\",\"protocol\":\"tcp\"}]'"),
|
|
809
|
+
env_vars: z.string().optional().describe("JSON object string of environment variables"),
|
|
810
|
+
volumes: z.string().optional().describe("JSON object string of host:container volume mappings"),
|
|
811
|
+
cpu_limit: z.number().optional().describe("CPU limit in cores"),
|
|
812
|
+
memory_limit: z.number().optional().describe("Memory limit in bytes"),
|
|
813
|
+
replicas: z.number().optional().describe("Replica count"),
|
|
814
|
+
restart_policy: z.string().optional().describe("e.g. 'unless-stopped', 'always'"),
|
|
815
|
+
command: z.string().optional().describe("Override the image command"),
|
|
816
|
+
entrypoint: z.string().optional().describe("Override the image entrypoint"),
|
|
817
|
+
health_check: z.string().optional().describe("JSON object string describing the healthcheck"),
|
|
818
|
+
}, async ({ stack_id, ...fields }) => {
|
|
819
|
+
const res = await api("POST", `/admin/stacks/${stack_id}/containers`, null, body(fields));
|
|
820
|
+
return { content: text(res) };
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
server.tool("lattice_update_container", "Update a container definition. Changes the stored record — redeploy or recreate the container for them to take effect", {
|
|
824
|
+
id: z.number().describe("Container ID"),
|
|
825
|
+
name: z.string().optional(),
|
|
826
|
+
image: z.string().optional(),
|
|
827
|
+
tag: z.string().optional(),
|
|
828
|
+
status: z.string().optional(),
|
|
829
|
+
port_mappings: z.string().optional().describe("JSON array string"),
|
|
830
|
+
env_vars: z.string().optional().describe("JSON object string"),
|
|
831
|
+
volumes: z.string().optional().describe("JSON object string"),
|
|
832
|
+
cpu_limit: z.number().optional(),
|
|
833
|
+
memory_limit: z.number().optional(),
|
|
834
|
+
replicas: z.number().optional(),
|
|
835
|
+
restart_policy: z.string().optional(),
|
|
836
|
+
command: z.string().optional(),
|
|
837
|
+
entrypoint: z.string().optional(),
|
|
838
|
+
}, async ({ id, ...fields }) => {
|
|
839
|
+
const res = await api("PUT", `/admin/containers/${id}`, null, body(fields));
|
|
840
|
+
return { content: text(res) };
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
server.tool("lattice_delete_container", "Delete a container definition and its running container. Destructive — lattice_stop_container only stops it", {
|
|
844
|
+
id: z.number().describe("Container ID"),
|
|
845
|
+
}, async ({ id }) => {
|
|
846
|
+
const res = await api("DELETE", `/admin/containers/${id}`);
|
|
847
|
+
return { content: text(res) };
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
851
|
+
// Workers — registration, tokens, volumes, networks
|
|
852
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
853
|
+
|
|
854
|
+
server.tool("lattice_create_worker", "Register a worker. The machine still needs the runner installed and a worker token before it connects", {
|
|
855
|
+
name: z.string().describe("Worker name"),
|
|
856
|
+
hostname: z.string().describe("Hostname"),
|
|
857
|
+
ip_address: z.string().optional().describe("IP address"),
|
|
858
|
+
labels: z.string().optional().describe("JSON object string of labels used by placement constraints"),
|
|
859
|
+
}, async (args) => {
|
|
860
|
+
const res = await api("POST", "/admin/workers", null, body(args));
|
|
861
|
+
return { content: text(res) };
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
server.tool("lattice_update_worker", "Update a worker's name, hostname, IP, status, labels or active flag", {
|
|
865
|
+
id: z.number().describe("Worker ID"),
|
|
866
|
+
name: z.string().optional(),
|
|
867
|
+
hostname: z.string().optional(),
|
|
868
|
+
ip_address: z.string().optional(),
|
|
869
|
+
status: z.string().optional(),
|
|
870
|
+
labels: z.string().optional().describe("JSON object string of labels"),
|
|
871
|
+
active: z.boolean().optional(),
|
|
872
|
+
}, async ({ id, ...fields }) => {
|
|
873
|
+
const res = await api("PUT", `/admin/workers/${id}`, null, body(fields));
|
|
874
|
+
return { content: text(res) };
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
server.tool("lattice_delete_worker", "Delete a worker. Every stack pinned to it becomes undeployable. Destructive", {
|
|
878
|
+
id: z.number().describe("Worker ID"),
|
|
879
|
+
}, async ({ id }) => {
|
|
880
|
+
const res = await api("DELETE", `/admin/workers/${id}`);
|
|
881
|
+
return { content: text(res) };
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
server.tool("lattice_get_worker_container_stats", "Get live per-container resource stats from one worker", {
|
|
885
|
+
id: z.number().describe("Worker ID"),
|
|
886
|
+
}, async ({ id }) => {
|
|
887
|
+
const res = await api("GET", `/admin/workers/${id}/container-stats`);
|
|
888
|
+
return { content: text(res) };
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
server.tool("lattice_list_worker_tokens", "List a worker's registration tokens. Values are never returned", {
|
|
892
|
+
id: z.number().describe("Worker ID"),
|
|
893
|
+
}, async ({ id }) => {
|
|
894
|
+
const res = await api("GET", `/admin/workers/${id}/tokens`);
|
|
895
|
+
return { content: text(res) };
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
server.tool("lattice_create_worker_token", "Create a registration token for a worker, used by the runner to connect. Plaintext is returned once", {
|
|
899
|
+
id: z.number().describe("Worker ID"),
|
|
900
|
+
name: z.string().describe("Token name"),
|
|
901
|
+
}, async ({ id, name }) => {
|
|
902
|
+
const res = await api("POST", `/admin/workers/${id}/tokens`, null, { name });
|
|
903
|
+
return { content: text(res) };
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
server.tool("lattice_delete_worker_token", "Delete a worker token. If the runner is using it, it cannot reconnect after a restart. Destructive", {
|
|
907
|
+
token_id: z.number().describe("Worker token ID"),
|
|
908
|
+
}, async ({ token_id }) => {
|
|
909
|
+
const res = await api("DELETE", `/admin/worker-tokens/${token_id}`);
|
|
910
|
+
return { content: text(res) };
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
server.tool("lattice_list_worker_volumes", "List Docker volumes on a worker", {
|
|
914
|
+
id: z.number().describe("Worker ID"),
|
|
915
|
+
}, async ({ id }) => {
|
|
916
|
+
const res = await api("GET", `/admin/workers/${id}/volumes`);
|
|
917
|
+
return { content: text(res) };
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
server.tool("lattice_create_worker_volume", "Create a Docker volume on a worker", {
|
|
921
|
+
id: z.number().describe("Worker ID"),
|
|
922
|
+
name: z.string().describe("Volume name"),
|
|
923
|
+
driver: z.string().optional().describe("Volume driver (defaults to 'local')"),
|
|
924
|
+
}, async ({ id, name, driver }) => {
|
|
925
|
+
const res = await api("POST", `/admin/workers/${id}/volumes`, null, body({ name, driver }));
|
|
926
|
+
return { content: text(res) };
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
server.tool("lattice_delete_worker_volume", "Delete a Docker volume on a worker. Any data stored in it is destroyed. Destructive", {
|
|
930
|
+
id: z.number().describe("Worker ID"),
|
|
931
|
+
name: z.string().describe("Volume name"),
|
|
932
|
+
}, async ({ id, name }) => {
|
|
933
|
+
const res = await api("DELETE", `/admin/workers/${id}/volumes/${encodeURIComponent(name)}`);
|
|
934
|
+
return { content: text(res) };
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
server.tool("lattice_list_worker_networks", "List Docker networks on a worker", {
|
|
938
|
+
id: z.number().describe("Worker ID"),
|
|
939
|
+
}, async ({ id }) => {
|
|
940
|
+
const res = await api("GET", `/admin/workers/${id}/networks`);
|
|
941
|
+
return { content: text(res) };
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
server.tool("lattice_create_worker_network", "Create a Docker network on a worker", {
|
|
945
|
+
id: z.number().describe("Worker ID"),
|
|
946
|
+
name: z.string().describe("Network name"),
|
|
947
|
+
driver: z.string().optional().describe("Network driver (defaults to 'bridge')"),
|
|
948
|
+
}, async ({ id, name, driver }) => {
|
|
949
|
+
const res = await api("POST", `/admin/workers/${id}/networks`, null, body({ name, driver }));
|
|
950
|
+
return { content: text(res) };
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
server.tool("lattice_delete_worker_network", "Delete a Docker network on a worker. Containers attached to it lose connectivity. Destructive", {
|
|
954
|
+
id: z.number().describe("Worker ID"),
|
|
955
|
+
name: z.string().describe("Network name"),
|
|
956
|
+
}, async ({ id, name }) => {
|
|
957
|
+
const res = await api("DELETE", `/admin/workers/${id}/networks/${encodeURIComponent(name)}`);
|
|
958
|
+
return { content: text(res) };
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
server.tool("lattice_list_all_networks", "List every tracked Docker network across the fleet", {}, async () => {
|
|
962
|
+
const res = await api("GET", "/admin/networks");
|
|
963
|
+
return { content: text(res) };
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
server.tool("lattice_delete_network", "Delete a tracked network by its Lattice ID. Destructive", {
|
|
967
|
+
id: z.number().describe("Network ID"),
|
|
968
|
+
}, async ({ id }) => {
|
|
969
|
+
const res = await api("DELETE", `/admin/networks/${id}`);
|
|
970
|
+
return { content: text(res) };
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
server.tool("lattice_force_remove_container", "Force-remove a container on a worker by name, bypassing the normal lifecycle. For clearing a wedged container that ordinary remove will not shift. Destructive", {
|
|
974
|
+
id: z.number().describe("Worker ID"),
|
|
975
|
+
name: z.string().describe("Container name to force-remove"),
|
|
976
|
+
}, async ({ id, name }) => {
|
|
977
|
+
const res = await api("POST", `/admin/workers/${id}/force-remove`, { name });
|
|
978
|
+
return { content: text(res) };
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
982
|
+
// Global env vars, templates, webhooks
|
|
983
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
984
|
+
|
|
985
|
+
server.tool("lattice_list_env_vars", "List global environment variables available for interpolation into stack and container configs as ${NAME}. Values marked is_secret are masked", {}, async () => {
|
|
986
|
+
const res = await api("GET", "/admin/env-vars");
|
|
987
|
+
return { content: text(res) };
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
server.tool("lattice_create_env_var", "Create a global environment variable", {
|
|
991
|
+
key: z.string().describe("Variable name, referenced as ${KEY}"),
|
|
992
|
+
value: z.string().describe("Value"),
|
|
993
|
+
is_secret: z.boolean().optional().describe("Mask the value in listings"),
|
|
994
|
+
}, async (args) => {
|
|
995
|
+
const res = await api("POST", "/admin/env-vars", null, body(args));
|
|
996
|
+
return { content: text(res) };
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
server.tool("lattice_update_env_var", "Update a global environment variable's value or secret flag. Containers pick it up on their next deploy", {
|
|
1000
|
+
id: z.number().describe("Env var ID"),
|
|
1001
|
+
value: z.string().optional().describe("New value"),
|
|
1002
|
+
is_secret: z.boolean().optional().describe("Mask the value in listings"),
|
|
1003
|
+
}, async ({ id, ...fields }) => {
|
|
1004
|
+
const res = await api("PUT", `/admin/env-vars/${id}`, null, body(fields));
|
|
1005
|
+
return { content: text(res) };
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
server.tool("lattice_delete_env_var", "Delete a global environment variable. Any config referencing ${KEY} fails to interpolate on next deploy. Destructive", {
|
|
1009
|
+
id: z.number().describe("Env var ID"),
|
|
1010
|
+
}, async ({ id }) => {
|
|
1011
|
+
const res = await api("DELETE", `/admin/env-vars/${id}`);
|
|
1012
|
+
return { content: text(res) };
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
server.tool("lattice_list_templates", "List saved stack templates", {}, async () => {
|
|
1016
|
+
const res = await api("GET", "/admin/templates");
|
|
1017
|
+
return { content: text(res) };
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
server.tool("lattice_create_template", "Create a stack template from a config document", {
|
|
1021
|
+
name: z.string().describe("Template name"),
|
|
1022
|
+
config: z.string().describe("Template configuration as a JSON string"),
|
|
1023
|
+
description: z.string().optional().describe("Description"),
|
|
1024
|
+
}, async (args) => {
|
|
1025
|
+
const res = await api("POST", "/admin/templates", null, body(args));
|
|
1026
|
+
return { content: text(res) };
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
server.tool("lattice_delete_template", "Delete a stack template. Destructive", {
|
|
1030
|
+
id: z.number().describe("Template ID"),
|
|
1031
|
+
}, async ({ id }) => {
|
|
1032
|
+
const res = await api("DELETE", `/admin/templates/${id}`);
|
|
1033
|
+
return { content: text(res) };
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
server.tool("lattice_list_webhooks", "List outbound webhooks that fire on fleet events", {}, async () => {
|
|
1037
|
+
const res = await api("GET", "/admin/webhooks");
|
|
1038
|
+
return { content: text(res) };
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
server.tool("lattice_create_webhook", "Create an outbound webhook. events is a JSON array string, e.g. '[\"container.status\"]' or '[\"*\"]' for everything", {
|
|
1042
|
+
name: z.string().describe("Webhook name"),
|
|
1043
|
+
url: z.string().describe("Destination URL"),
|
|
1044
|
+
events: z.string().describe("JSON array string of event types, or '[\"*\"]' for all"),
|
|
1045
|
+
secret: z.string().optional().describe("Shared secret used to sign deliveries"),
|
|
1046
|
+
}, async (args) => {
|
|
1047
|
+
const res = await api("POST", "/admin/webhooks", null, body(args));
|
|
1048
|
+
return { content: text(res) };
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
server.tool("lattice_update_webhook", "Update a webhook's URL, event list, secret or active flag", {
|
|
1052
|
+
id: z.number().describe("Webhook ID"),
|
|
1053
|
+
name: z.string().optional(),
|
|
1054
|
+
url: z.string().optional(),
|
|
1055
|
+
events: z.string().optional().describe("JSON array string of event types"),
|
|
1056
|
+
secret: z.string().optional(),
|
|
1057
|
+
active: z.boolean().optional(),
|
|
1058
|
+
}, async ({ id, ...fields }) => {
|
|
1059
|
+
const res = await api("PUT", `/admin/webhooks/${id}`, null, body(fields));
|
|
1060
|
+
return { content: text(res) };
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
server.tool("lattice_delete_webhook", "Delete a webhook. Destructive", {
|
|
1064
|
+
id: z.number().describe("Webhook ID"),
|
|
1065
|
+
}, async ({ id }) => {
|
|
1066
|
+
const res = await api("DELETE", `/admin/webhooks/${id}`);
|
|
1067
|
+
return { content: text(res) };
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
server.tool("lattice_test_webhook", "Send a test payload to a webhook to verify the endpoint accepts it", {
|
|
1071
|
+
id: z.number().describe("Webhook ID"),
|
|
1072
|
+
}, async ({ id }) => {
|
|
1073
|
+
const res = await api("POST", `/admin/webhooks/${id}/test`);
|
|
1074
|
+
return { content: text(res) };
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1078
|
+
// Users & instance configuration
|
|
1079
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1080
|
+
|
|
1081
|
+
server.tool("lattice_list_users", "List Lattice users with roles and status", {}, async () => {
|
|
1082
|
+
const res = await api("GET", "/admin/users");
|
|
1083
|
+
return { content: text(res) };
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
server.tool("lattice_create_user", "Create a local Lattice user", {
|
|
1087
|
+
email: z.string().describe("Email address"),
|
|
1088
|
+
password: z.string().describe("Initial password"),
|
|
1089
|
+
role: z.string().describe("Role, e.g. 'admin', 'editor', 'viewer'"),
|
|
1090
|
+
name: z.string().optional().describe("Display name"),
|
|
1091
|
+
}, async (args) => {
|
|
1092
|
+
const res = await api("POST", "/admin/users", null, body(args));
|
|
1093
|
+
return { content: text(res) };
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
server.tool("lattice_update_user", "Update a user's name, role or active flag. Setting active=false blocks their access immediately", {
|
|
1097
|
+
id: z.number().describe("User ID"),
|
|
1098
|
+
name: z.string().optional(),
|
|
1099
|
+
role: z.string().optional(),
|
|
1100
|
+
active: z.boolean().optional(),
|
|
1101
|
+
}, async ({ id, ...fields }) => {
|
|
1102
|
+
const res = await api("PUT", `/admin/users/${id}`, null, body(fields));
|
|
1103
|
+
return { content: text(res) };
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
server.tool("lattice_delete_user", "Delete a Lattice user. Destructive", {
|
|
1107
|
+
id: z.number().describe("User ID"),
|
|
1108
|
+
}, async ({ id }) => {
|
|
1109
|
+
const res = await api("DELETE", `/admin/users/${id}`);
|
|
1110
|
+
return { content: text(res) };
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
server.tool("lattice_get_sso_config", "Get the Forta SSO configuration", {}, async () => {
|
|
1114
|
+
const res = await api("GET", "/admin/sso-config");
|
|
1115
|
+
return { content: text(res) };
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
server.tool("lattice_update_sso_config", "Update the SSO configuration. Misconfiguring this can lock every SSO user out of Lattice — verify local admin access first", {
|
|
1119
|
+
enabled: z.boolean().optional(),
|
|
1120
|
+
client_id: z.string().optional(),
|
|
1121
|
+
client_secret: z.string().optional(),
|
|
1122
|
+
authorize_url: z.string().optional(),
|
|
1123
|
+
token_url: z.string().optional(),
|
|
1124
|
+
userinfo_url: z.string().optional(),
|
|
1125
|
+
redirect_url: z.string().optional(),
|
|
1126
|
+
logout_url: z.string().optional(),
|
|
1127
|
+
scopes: z.string().optional(),
|
|
1128
|
+
user_identifier: z.string().optional().describe("Claim used to identify the user, e.g. 'email' or 'sub'"),
|
|
1129
|
+
button_label: z.string().optional(),
|
|
1130
|
+
auto_provision: z.boolean().optional().describe("Create a local user on first SSO login"),
|
|
1131
|
+
post_login_url: z.string().optional(),
|
|
1132
|
+
}, async (args) => {
|
|
1133
|
+
const res = await api("PUT", "/admin/sso-config", null, body(args));
|
|
1134
|
+
return { content: text(res) };
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
server.tool("lattice_get_smtp_config", "Get the SMTP configuration used for alert emails", {}, async () => {
|
|
1138
|
+
const res = await api("GET", "/admin/smtp-config");
|
|
1139
|
+
return { content: text(res) };
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
server.tool("lattice_update_smtp_config", "Update the SMTP configuration", {
|
|
1143
|
+
enabled: z.boolean().optional(),
|
|
1144
|
+
host: z.string().optional(),
|
|
1145
|
+
port: z.string().optional().describe("Port as a string, matching the API"),
|
|
1146
|
+
username: z.string().optional(),
|
|
1147
|
+
password: z.string().optional(),
|
|
1148
|
+
from_email: z.string().optional(),
|
|
1149
|
+
from_name: z.string().optional(),
|
|
1150
|
+
recipients: z.string().optional().describe("Comma-separated recipient list"),
|
|
1151
|
+
}, async (args) => {
|
|
1152
|
+
const res = await api("PUT", "/admin/smtp-config", null, body(args));
|
|
1153
|
+
return { content: text(res) };
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
server.tool("lattice_test_smtp", "Send a test email using the saved SMTP configuration", {}, async () => {
|
|
1157
|
+
const res = await api("POST", "/admin/smtp-config/test");
|
|
1158
|
+
return { content: text(res) };
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
server.tool("lattice_get_notification_prefs", "Get per-event notification preferences", {}, async () => {
|
|
1162
|
+
const res = await api("GET", "/admin/notification-prefs");
|
|
1163
|
+
return { content: text(res) };
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
server.tool("lattice_update_notification_prefs", "Update notification preferences. Merges into existing prefs; unknown event types are ignored rather than rejected, so verify with lattice_get_notification_prefs afterwards", {
|
|
1167
|
+
preferences: z.record(z.any()).describe("Object keyed by event type, e.g. {\"container.status\":{\"email\":true}}"),
|
|
1168
|
+
}, async ({ preferences }) => {
|
|
1169
|
+
const res = await api("PUT", "/admin/notification-prefs", null, preferences);
|
|
1170
|
+
return { content: text(res) };
|
|
1171
|
+
});
|
|
1172
|
+
|
|
354
1173
|
// --- Start ---
|
|
355
1174
|
|
|
356
1175
|
const transport = new StdioServerTransport();
|