lattice-mcp 0.1.0__tar.gz
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.
- lattice_mcp-0.1.0/DESIGN.md +397 -0
- lattice_mcp-0.1.0/Makefile +27 -0
- lattice_mcp-0.1.0/PKG-INFO +10 -0
- lattice_mcp-0.1.0/README.md +111 -0
- lattice_mcp-0.1.0/lattice-scrape/scrape.graphql.json +1 -0
- lattice_mcp-0.1.0/lattice-scrape/scrape.html +772 -0
- lattice_mcp-0.1.0/lattice-scrape/scrape.meta.json +4 -0
- lattice_mcp-0.1.0/lattice-scrape/scrape.txt +8 -0
- lattice_mcp-0.1.0/lattice-scrape-root/scrape.graphql.json +531 -0
- lattice_mcp-0.1.0/lattice-scrape-root/scrape.html +327 -0
- lattice_mcp-0.1.0/lattice-scrape-root/scrape.meta.json +4 -0
- lattice_mcp-0.1.0/lattice-scrape-root/scrape.txt +49 -0
- lattice_mcp-0.1.0/pyproject.toml +28 -0
- lattice_mcp-0.1.0/src/lattice/__init__.py +1 -0
- lattice_mcp-0.1.0/src/lattice/__main__.py +3 -0
- lattice_mcp-0.1.0/src/lattice/auth.py +99 -0
- lattice_mcp-0.1.0/src/lattice/cli.py +118 -0
- lattice_mcp-0.1.0/src/lattice/client.py +59 -0
- lattice_mcp-0.1.0/src/lattice/credentials.py +128 -0
- lattice_mcp-0.1.0/src/lattice/mcp_server.py +369 -0
- lattice_mcp-0.1.0/src/lattice/ui.py +287 -0
- lattice_mcp-0.1.0/tests/test_auth.py +102 -0
- lattice_mcp-0.1.0/tests/test_client.py +80 -0
- lattice_mcp-0.1.0/tests/test_credentials.py +132 -0
- lattice_mcp-0.1.0/tests/test_ui.py +111 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# Lattice Automation
|
|
2
|
+
|
|
3
|
+
> Package lives at `/git/lattice/` (standalone repo).
|
|
4
|
+
> SSO login completed via Playwright headed browser or Chrome CDP.
|
|
5
|
+
> Session stored at `~/.config/lattice/browser-state.json`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Status: Working
|
|
10
|
+
|
|
11
|
+
All milestones achieved:
|
|
12
|
+
|
|
13
|
+
- [x] Package built at `lattice/` — 64 unit tests passing
|
|
14
|
+
- [x] `pip install -e ".[dev,ui]" && playwright install chromium`
|
|
15
|
+
- [x] SSO login via headed Playwright or CDP (`--cdp-url http://127.0.0.1:9223`)
|
|
16
|
+
- [x] Scrape `/` (root is authenticated home, not `/home`)
|
|
17
|
+
- [x] GraphQL capture working (4+ responses per page)
|
|
18
|
+
- [x] Read objectives via GraphQL (`viewer.goals.edges`)
|
|
19
|
+
- [x] **Write objectives** — posted update via `AddUpdateToGoalMutation`
|
|
20
|
+
- [x] **MCP server** — full CRUD: create, list, update, delete objectives
|
|
21
|
+
- [x] Async Playwright (required for MCP's async event loop)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Goal
|
|
26
|
+
|
|
27
|
+
Programmatic Lattice access **without** a Lattice admin API key:
|
|
28
|
+
|
|
29
|
+
1. Manual SSO in a real browser (Playwright headed or Chrome CDP)
|
|
30
|
+
2. Persist Playwright `storage_state`
|
|
31
|
+
3. Scrape pages + **capture GraphQL** JSON for automation
|
|
32
|
+
4. Parsers → CLI → optional MCP
|
|
33
|
+
|
|
34
|
+
Secondary (only if admin issues a key): Bearer API against `api.latticehq.com`.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Discovered routes and GraphQL
|
|
39
|
+
|
|
40
|
+
### Working routes
|
|
41
|
+
|
|
42
|
+
| Path | Content |
|
|
43
|
+
|------|---------|
|
|
44
|
+
| `/` | Authenticated home dashboard (NOT `/home` — that 404s) |
|
|
45
|
+
| `/goals` | Company-wide objectives (14696 total, paginated) |
|
|
46
|
+
| `/goals/explore?ownerEntityIdsFilter=["{userId}"]&statusesFilter=["AllActive"]&viewType=cascade` | Personal objectives filtered by owner |
|
|
47
|
+
| `/goals/{entityId}` | Single objective detail page |
|
|
48
|
+
| `/grow` | Grow page (loaded successfully) |
|
|
49
|
+
|
|
50
|
+
### GraphQL operations captured
|
|
51
|
+
|
|
52
|
+
**Read (from `/goals` page):**
|
|
53
|
+
- `viewer.goals.edges[].node` — objective list with: `entityId`, `name`, `priority`, `dueDate`, `status` (green/amber/red/null), `computedProgressAmount`, `owners[].name`, `lastUpdate`, `okrType`, `state`
|
|
54
|
+
- `viewer.allGoalsAnalytics.goalStatusBreakdown` — counts by status
|
|
55
|
+
- `viewer.user` — current user info (entityId, name, title, email, isAdmin)
|
|
56
|
+
- `viewer.company.goalCycles` — available fiscal quarters
|
|
57
|
+
|
|
58
|
+
**Write (from goal detail page "Update" button):**
|
|
59
|
+
|
|
60
|
+
```graphql
|
|
61
|
+
mutation AddUpdateToGoalMutation($input: AddUpdateToGoalInput!) {
|
|
62
|
+
addUpdateToGoal(input: $input) {
|
|
63
|
+
goal { ... }
|
|
64
|
+
goalUpdate { entityId ... }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Variables:
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"input": {
|
|
73
|
+
"goalId": "<base64 goal ID>",
|
|
74
|
+
"data": {
|
|
75
|
+
"comment": "<update text with \\n<br>\\n for newlines>",
|
|
76
|
+
"status": "green"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `goalId` is the base64-encoded relay ID (NOT the entityId UUID)
|
|
83
|
+
- Status values: `"green"` (On track), `"amber"` (Progressing), `"red"` (Off track)
|
|
84
|
+
- Newlines in comment become `\n<br>\n` in the mutation payload
|
|
85
|
+
- Introspection is disabled — mutations must be discovered by UI interaction
|
|
86
|
+
|
|
87
|
+
### Key IDs
|
|
88
|
+
|
|
89
|
+
| Entity | Value |
|
|
90
|
+
|--------|-------|
|
|
91
|
+
| User (Sean Ahn) | `01bfc19d-5192-4939-b03d-d185138ce183` |
|
|
92
|
+
| Company (C3 AI) | `293aacd4-f240-4356-8b47-34247c10420f` |
|
|
93
|
+
| Socket Firewall objective | `c876f39d-374d-42c7-84e5-d8feec38501c` |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Jira integration
|
|
98
|
+
|
|
99
|
+
Jira API token at `~/.ssh/jira-jupyer` (Atlassian Cloud basic auth).
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
curl -s -u "sean.ahn@c3.ai:$(cat ~/.ssh/jira-jupyer)" \
|
|
103
|
+
-X POST "https://c3energy.atlassian.net/rest/api/3/search/jql" \
|
|
104
|
+
-H "Content-Type: application/json" \
|
|
105
|
+
-d '{"jql": "text ~ \"5077-US7\"", "maxResults": 5}'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Linked tickets for the Socket Firewall objective:
|
|
109
|
+
|
|
110
|
+
| Key | Summary | Assignee |
|
|
111
|
+
|-----|---------|----------|
|
|
112
|
+
| INFOSEC-6098 | [5077-US7] Configure Cursor and Claude Code clients to route package installs through Socket Firewall | Seung Ahn |
|
|
113
|
+
| INFOSEC-8167 | [5077-US7] Deliverable Set A — Local agent client config (Cursor local, Claude Code local) | infosec-jira |
|
|
114
|
+
| INFOSEC-8168 | [5077-US7] Deliverable Set B — SaaS mode control assessment (Cursor SaaS, Claude Code SaaS) | infosec-jira |
|
|
115
|
+
|
|
116
|
+
Note: the old `/rest/api/3/search` endpoint is removed — use `/rest/api/3/search/jql` with POST body.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Automation recipe (what worked)
|
|
121
|
+
|
|
122
|
+
### Update an objective from Jira data
|
|
123
|
+
|
|
124
|
+
1. Fetch Jira tickets: `POST /rest/api/3/search/jql` with `{"jql": "..."}`
|
|
125
|
+
2. Get ticket details: `GET /rest/api/3/issue/{id}?fields=summary,description,status`
|
|
126
|
+
3. Open goal page in Playwright with saved storage state
|
|
127
|
+
4. Click "Update" button → select status radio → type comment → click second "Update" button
|
|
128
|
+
5. GraphQL mutation fires automatically; update is saved
|
|
129
|
+
|
|
130
|
+
### CDP login (port 9222 taken by Cursor — use 9223)
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
google-chrome --remote-debugging-port=9223 --user-data-dir=/tmp/chrome-lattice --no-first-run &
|
|
134
|
+
# Complete SSO in browser, then:
|
|
135
|
+
python3 -m lattice ui login --cdp-url http://127.0.0.1:9223
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Decision log (do not re-litigate)
|
|
141
|
+
|
|
142
|
+
| Topic | Conclusion |
|
|
143
|
+
|-------|------------|
|
|
144
|
+
| Official Lattice MCP | None. Community MCPs need API keys. |
|
|
145
|
+
| Non-admin API keys | **Impossible** — Admin → Platform → API keys only |
|
|
146
|
+
| Web SSO → API access | **No** |
|
|
147
|
+
| Cookie as `Authorization: Bearer` | **No** — not a Talent API key |
|
|
148
|
+
| narwhalChat Lattice | Exists: GraphQL to `https://c3.latticehq.com/` via `latticeToken` **runtime arg**; token **not in git** |
|
|
149
|
+
| Steal token from narwhalChat | **Cannot** |
|
|
150
|
+
| Approach | **UI scrape + GraphQL capture** after manual SSO |
|
|
151
|
+
| Rejected | Automating IdP login; cookie-as-API-key; DOM as stable public API |
|
|
152
|
+
|
|
153
|
+
Tenant: `c3.latticehq.com`
|
|
154
|
+
Talent API (admin path): `https://api.latticehq.com`
|
|
155
|
+
narwhalChat code (reference only): `c3engineering/repo/community/narwhalChat/src/lattice/`
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Rebuild spec
|
|
160
|
+
|
|
161
|
+
Package location: `/git/lattice/` (standalone repo, built 2026-07-24, MCP added 2026-07-25).
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
lattice/
|
|
165
|
+
README.md
|
|
166
|
+
DESIGN.md
|
|
167
|
+
pyproject.toml
|
|
168
|
+
src/lattice/
|
|
169
|
+
__init__.py
|
|
170
|
+
__main__.py
|
|
171
|
+
cli.py
|
|
172
|
+
auth.py
|
|
173
|
+
credentials.py
|
|
174
|
+
client.py
|
|
175
|
+
ui.py
|
|
176
|
+
mcp_server.py
|
|
177
|
+
tests/
|
|
178
|
+
test_credentials.py
|
|
179
|
+
test_client.py
|
|
180
|
+
test_auth.py
|
|
181
|
+
test_ui.py
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `pyproject.toml`
|
|
185
|
+
|
|
186
|
+
```toml
|
|
187
|
+
[build-system]
|
|
188
|
+
requires = ["hatchling"]
|
|
189
|
+
build-backend = "hatchling.build"
|
|
190
|
+
|
|
191
|
+
[project]
|
|
192
|
+
name = "lattice-cli"
|
|
193
|
+
version = "0.1.0"
|
|
194
|
+
description = "CLI for Lattice — API-key auth and Playwright UI scraping"
|
|
195
|
+
requires-python = ">=3.10"
|
|
196
|
+
license = { text = "Proprietary" }
|
|
197
|
+
dependencies = []
|
|
198
|
+
|
|
199
|
+
[project.optional-dependencies]
|
|
200
|
+
dev = ["pytest>=7.0"]
|
|
201
|
+
ui = ["playwright>=1.40"]
|
|
202
|
+
|
|
203
|
+
[project.scripts]
|
|
204
|
+
lattice = "lattice.cli:main"
|
|
205
|
+
|
|
206
|
+
[tool.hatch.build.targets.wheel]
|
|
207
|
+
packages = ["src/lattice"]
|
|
208
|
+
|
|
209
|
+
[tool.pytest.ini_options]
|
|
210
|
+
testpaths = ["tests"]
|
|
211
|
+
pythonpath = ["src"]
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Constants / config
|
|
215
|
+
|
|
216
|
+
| Name | Value |
|
|
217
|
+
|------|--------|
|
|
218
|
+
| Default web host | `c3.latticehq.com` |
|
|
219
|
+
| Default API URL | `https://api.latticehq.com` |
|
|
220
|
+
| Config dir | `~/.config/lattice` (override: `--config-dir` or `LATTICE_CONFIG_DIR`) |
|
|
221
|
+
| API creds file | `credentials.json` mode `0600` |
|
|
222
|
+
| UI session file | `browser-state.json` mode `0600` |
|
|
223
|
+
| Env API token | `LATTICE_API_TOKEN` or `LATTICE_TOKEN` |
|
|
224
|
+
| Env API URL | `LATTICE_API_URL` |
|
|
225
|
+
| Env web host | `LATTICE_WEB_HOSTNAME` |
|
|
226
|
+
|
|
227
|
+
### Module: `credentials.py`
|
|
228
|
+
|
|
229
|
+
- `@dataclass Credentials(token, api_url, web_hostname, user: dict)`
|
|
230
|
+
- `masked_token()`, `config_dir()`, `credentials_path()`, `load()`, `save()`, `clear()`
|
|
231
|
+
- `load()`: env token overrides file; corrupt file ignored if env set
|
|
232
|
+
- `save()`: write via temp file + `os.replace`, chmod `0600`
|
|
233
|
+
|
|
234
|
+
### Module: `client.py` (API path; stdlib `urllib`)
|
|
235
|
+
|
|
236
|
+
- `get_me(api_url, token) -> dict` → `GET {api_url}/v1/me` with `Authorization: Bearer …`
|
|
237
|
+
- `LatticeAPIError(status, message)`
|
|
238
|
+
- `user_display(user) -> str` — name + email fallbacks (`preferredName`/`firstName`/`lastName`/`workEmail`)
|
|
239
|
+
|
|
240
|
+
### Module: `auth.py` (API path)
|
|
241
|
+
|
|
242
|
+
| Command | Behavior |
|
|
243
|
+
|---------|----------|
|
|
244
|
+
| `login` | Optional webbrowser open to `https://{host}/admin`; `getpass` or `--with-token`; validate `/v1/me`; `save()` |
|
|
245
|
+
| `status` | Load creds; refresh via `/v1/me` unless offline; print user |
|
|
246
|
+
| `logout` | `clear()` |
|
|
247
|
+
| `token` | Print raw token to stdout |
|
|
248
|
+
|
|
249
|
+
### Module: `ui.py` (primary path; Playwright optional import)
|
|
250
|
+
|
|
251
|
+
**Login detection:** URL is “still logging in” if host ≠ tenant (e.g. Okta) OR path/host contains any of:
|
|
252
|
+
`login`, `signin`, `sign-in`, `sso`, `saml`, `oauth`, `okta`, `auth0`, `openid`, `callback`.
|
|
253
|
+
|
|
254
|
+
**Authenticated:** on tenant hostname, not a login URL, and context has cookies.
|
|
255
|
+
|
|
256
|
+
| Command | Behavior |
|
|
257
|
+
|---------|----------|
|
|
258
|
+
| `ui login` | Launch Chromium **headed** by default; goto `https://{hostname}`; wait up to `--timeout` (default 300s) for auth; `storage_state` → `browser-state.json` |
|
|
259
|
+
| `ui login --cdp-url URL` | `chromium.connect_over_cdp`; wait for auth; save storage; **do not** close user Chrome |
|
|
260
|
+
| `ui login --headless` | Allowed but usually useless for SSO |
|
|
261
|
+
| `ui status` | Report whether `browser-state.json` has cookies |
|
|
262
|
+
| `ui logout` | Delete `browser-state.json` |
|
|
263
|
+
| `ui scrape [path]` | Load storage state; navigate (path or full URL); `networkidle` + ~1.5s; if redirected to login → error “re-login”; dump outputs |
|
|
264
|
+
|
|
265
|
+
**Scrape outputs** (dir `--out`, default `./lattice-scrape`):
|
|
266
|
+
|
|
267
|
+
| File | Content |
|
|
268
|
+
|------|---------|
|
|
269
|
+
| `scrape.meta.json` | `{url, title}` |
|
|
270
|
+
| `scrape.txt` | `page.inner_text("body")` |
|
|
271
|
+
| `scrape.graphql.json` | List of `{url, status, body}` for responses whose URL contains `graphql` (unless `--no-graphql`) |
|
|
272
|
+
| `scrape.html` | Full HTML if `--html` |
|
|
273
|
+
| screenshot | If `--screenshot PATH` |
|
|
274
|
+
|
|
275
|
+
**Launch error UX:** On Playwright launch failure, print hints:
|
|
276
|
+
|
|
277
|
+
- `python3 -m playwright install-deps chromium`
|
|
278
|
+
- `python3 -m playwright install chromium`
|
|
279
|
+
- If no `DISPLAY`/`WAYLAND_DISPLAY`: explain headed SSO impossible; suggest laptop login or `--cdp-url`
|
|
280
|
+
|
|
281
|
+
**Do not** treat missing Playwright as a hard import at module load — fail with install instructions when UI commands run.
|
|
282
|
+
|
|
283
|
+
### Module: `cli.py`
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
lattice [--config-dir DIR] [--version]
|
|
287
|
+
auth login [--hostname] [--api-url] [--with-token] [--no-browser]
|
|
288
|
+
auth status [--show-token] [--offline]
|
|
289
|
+
auth logout | token
|
|
290
|
+
ui login [--hostname] [--timeout] [--headless] [--cdp-url]
|
|
291
|
+
ui status | logout
|
|
292
|
+
ui scrape [path] [--hostname] [--out] [--html] [--no-graphql]
|
|
293
|
+
[--screenshot] [--headed] [--print]
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`__main__.py`: `raise SystemExit(main())`
|
|
297
|
+
`__init__.py`: `__version__ = "0.1.0"`
|
|
298
|
+
|
|
299
|
+
### Tests (no live Lattice / no browser required)
|
|
300
|
+
|
|
301
|
+
Cover at least:
|
|
302
|
+
|
|
303
|
+
- credentials save/load/chmod/env override/clear/mask
|
|
304
|
+
- `get_me` success + 401 (`urllib` mocked)
|
|
305
|
+
- `user_display` variants
|
|
306
|
+
- auth login success/fail (mock `get_me`); status/logout/token; CLI login
|
|
307
|
+
- `looks_like_login_url` (Okta vs tenant `/home` vs `/login`)
|
|
308
|
+
- `session_exists` / `write_scrape_outputs`
|
|
309
|
+
- CLI `ui status` → 1 when no session
|
|
310
|
+
|
|
311
|
+
Target: ~20+ unit tests, `pytest` green offline.
|
|
312
|
+
|
|
313
|
+
### README (short)
|
|
314
|
+
|
|
315
|
+
Document: install `[ui]`, `playwright install chromium`, `install-deps`, headed login, CDP login, scrape flags, API auth secondary, never commit secrets/`lattice-scrape/`.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Local SSO recipes (this is the blocker)
|
|
320
|
+
|
|
321
|
+
### A. Headed Playwright (preferred on laptop)
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
cd c3securitytools/lattice
|
|
325
|
+
pip install -e ".[dev,ui]"
|
|
326
|
+
playwright install chromium
|
|
327
|
+
PYTHONPATH=src python3 -m lattice ui login
|
|
328
|
+
PYTHONPATH=src python3 -m lattice ui scrape /home --out ./lattice-scrape --html
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### B. System Chrome + CDP
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
# macOS
|
|
335
|
+
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
|
|
336
|
+
--remote-debugging-port=9222 \
|
|
337
|
+
--user-data-dir=/tmp/chrome-lattice
|
|
338
|
+
# SSO to https://c3.latticehq.com in that window, then:
|
|
339
|
+
PYTHONPATH=src python3 -m lattice ui login --cdp-url http://127.0.0.1:9222
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### C. Headless remote (avoid unless necessary)
|
|
343
|
+
|
|
344
|
+
Remote with no `DISPLAY` **cannot** complete SSO alone. Either run A/B on laptop, or:
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
# Laptop Chrome on 9222 + SSO, then:
|
|
348
|
+
ssh -R 9222:127.0.0.1:9222 user@remote
|
|
349
|
+
# On remote:
|
|
350
|
+
lattice ui login --cdp-url http://127.0.0.1:9222
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Failures already seen — don’t repeat blindly
|
|
354
|
+
|
|
355
|
+
- `libxkbcommon.so.0` missing → `playwright install-deps chromium` (fixed Chromium launch)
|
|
356
|
+
- `google-chrome: command not found` on remote
|
|
357
|
+
- `connect ECONNREFUSED 127.0.0.1:9222` — nothing listening; CDP must point at a live Chrome
|
|
358
|
+
- **Port 9222 taken by Cursor** (VS Code server) — use 9223 or another port
|
|
359
|
+
- `/home` route 404s — use `/` for authenticated home page
|
|
360
|
+
- GraphQL introspection disabled — cannot discover mutations via `__schema`
|
|
361
|
+
- Cookies alone via `urllib` get 403 — must execute GraphQL in-browser (CSRF protection)
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## Next steps
|
|
366
|
+
|
|
367
|
+
1. [x] ~~Build MCP server with CRUD objectives~~ (done — `lattice.mcp_server`)
|
|
368
|
+
2. [x] ~~Discover create/delete mutations~~ (done via UI automation)
|
|
369
|
+
3. [ ] Publish to PyPI as `lattice-mcp` for `uvx lattice-mcp` install
|
|
370
|
+
- Rename package in `pyproject.toml` from `lattice-cli` to `lattice-mcp`
|
|
371
|
+
- Add `[project.scripts]` entry: `lattice-mcp = "lattice.mcp_server:mcp.run"`
|
|
372
|
+
- Add `[project.gui-scripts]` or console script for `lattice` CLI
|
|
373
|
+
- Register on PyPI (needs account + API token)
|
|
374
|
+
- Users install with: `uvx lattice-mcp` or `pip install lattice-mcp`
|
|
375
|
+
4. [ ] List on [mcp.so](https://mcp.so) and/or [glama.ai/mcp](https://glama.ai/mcp) for discoverability
|
|
376
|
+
5. [ ] Push repo to GitHub (e.g. `github.com/sahn/lattice-mcp`)
|
|
377
|
+
6. [ ] Build `lattice goals` CLI command (list personal objectives with status)
|
|
378
|
+
7. [ ] Scrape more routes: `/updates`, `/feedback`, `/people` — map GraphQL ops
|
|
379
|
+
8. [ ] If org later grants API key: prefer REST (`auth` + `/v1/*`) over scrape
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Security
|
|
384
|
+
|
|
385
|
+
- `browser-state.json` ≡ full SSO session — secret, `0600`, never git
|
|
386
|
+
- Scrape dumps = HR/org PII — don’t commit `lattice-scrape/`
|
|
387
|
+
- Cookies ≠ API keys
|
|
388
|
+
- Confirm policy allows personal-SSO automation of Lattice
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## References
|
|
393
|
+
|
|
394
|
+
- Lattice API auth: https://developers.lattice.com/reference/authentication
|
|
395
|
+
- `GET /v1/me`: https://developers.lattice.com/reference/api_me-1
|
|
396
|
+
- narwhalChat Lattice (reference): `c3engineering/repo/community/narwhalChat/src/lattice/`
|
|
397
|
+
- Prior remote prototype lived on branch `lattice` in `c3securitytools` — **may not exist locally; rebuild from this doc**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.PHONY: install dev test lint clean build publish publish-test
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
pip install -e .
|
|
5
|
+
playwright install chromium
|
|
6
|
+
|
|
7
|
+
dev:
|
|
8
|
+
pip install -e ".[dev]"
|
|
9
|
+
playwright install chromium
|
|
10
|
+
|
|
11
|
+
test:
|
|
12
|
+
pytest
|
|
13
|
+
|
|
14
|
+
clean:
|
|
15
|
+
rm -rf dist/ build/ *.egg-info src/*.egg-info
|
|
16
|
+
|
|
17
|
+
build: clean
|
|
18
|
+
pip install --upgrade build
|
|
19
|
+
python -m build
|
|
20
|
+
|
|
21
|
+
publish: build
|
|
22
|
+
pip install --upgrade twine
|
|
23
|
+
twine upload dist/*
|
|
24
|
+
|
|
25
|
+
publish-test: build
|
|
26
|
+
pip install --upgrade twine
|
|
27
|
+
twine upload --repository testpypi dist/*
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lattice-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Lattice HR — manage objectives via browser session replay
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: mcp>=1.0
|
|
8
|
+
Requires-Dist: playwright>=1.40
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Lattice MCP Server
|
|
2
|
+
|
|
3
|
+
Automates Lattice HR objectives (create, update, delete, list) via browser session replay. No API key required — authenticates through SSO and persists the session for headless Playwright reuse.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Python 3.10+
|
|
8
|
+
- Chrome/Chromium (for SSO login)
|
|
9
|
+
- A display environment (local machine or X-forwarded) for initial login
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd lattice/
|
|
15
|
+
pip install -e ".[ui]"
|
|
16
|
+
playwright install chromium
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Authentication
|
|
21
|
+
|
|
22
|
+
The server uses a saved Playwright browser session (`~/.config/lattice/browser-state.json`). You must log in once via SSO to create it.
|
|
23
|
+
|
|
24
|
+
### Option A: Local machine with a display
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python3 -m lattice ui login
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A Chromium window opens — complete your SSO login. The window closes automatically once authenticated and the session is saved. The session is reusable until it expires on Lattice's side (typically days to weeks).
|
|
31
|
+
|
|
32
|
+
### Option B: Headless server (attach to running Chrome)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# On a machine with a display, start Chrome with remote debugging:
|
|
36
|
+
google-chrome --remote-debugging-port=9223 --user-data-dir=/tmp/chrome-lattice --no-first-run &
|
|
37
|
+
|
|
38
|
+
# Complete SSO in that browser, then capture the session:
|
|
39
|
+
python3 -m lattice ui login --cdp-url http://127.0.0.1:9223
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Session expiry
|
|
43
|
+
|
|
44
|
+
If tools return "Session expired", re-run `lattice ui login`.
|
|
45
|
+
|
|
46
|
+
## MCP Server Setup (Claude Code)
|
|
47
|
+
|
|
48
|
+
Add to your project's `.mcp.json`:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"lattice": {
|
|
54
|
+
"command": "/path/to/your/.venv/bin/python3",
|
|
55
|
+
"args": ["-m", "lattice.mcp_server"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Restart Claude Code to load the server. The tools appear as `lattice_*` in your session.
|
|
62
|
+
|
|
63
|
+
## Available Tools
|
|
64
|
+
|
|
65
|
+
| Tool | Description |
|
|
66
|
+
|------|-------------|
|
|
67
|
+
| `lattice_session_status` | Check if the browser session is active |
|
|
68
|
+
| `lattice_objectives` | List active objectives for a user (defaults to you) |
|
|
69
|
+
| `lattice_create_objective` | Create a new objective (title, optional priority/due date) |
|
|
70
|
+
| `lattice_update_objective` | Post a status update + comment to an objective |
|
|
71
|
+
| `lattice_delete_objective` | Delete an objective by entityId |
|
|
72
|
+
| `lattice_scrape` | Scrape any Lattice page and return visible text |
|
|
73
|
+
|
|
74
|
+
### Example usage (via Claude Code)
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
> list my lattice objectives
|
|
78
|
+
> create a lattice objective titled "Ship feature X"
|
|
79
|
+
> update objective <entityId> status green comment "Merged PR, deploying tomorrow"
|
|
80
|
+
> delete objective <entityId>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Multi-server workflow (with Jira MCP)
|
|
84
|
+
|
|
85
|
+
If you also have a Jira MCP server in your session, you can chain them:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
> fetch PLAT-0000 and PLAT-0001 from jira, then create lattice objectives from their summaries
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Claude calls the Jira server to get ticket details, then calls `lattice_create_objective` for each — no glue code needed.
|
|
92
|
+
|
|
93
|
+
## How It Works
|
|
94
|
+
|
|
95
|
+
1. Tools launch headless Chromium with the saved session cookies
|
|
96
|
+
2. Navigate to the relevant Lattice page
|
|
97
|
+
3. Interact with the UI (fill forms, click buttons) via Playwright
|
|
98
|
+
4. GraphQL mutations fire as a side-effect of the UI interaction
|
|
99
|
+
5. Cloudflare passes because the session includes valid clearance cookies
|
|
100
|
+
|
|
101
|
+
There is no direct API access — Lattice does not issue API keys to non-admins. See `DESIGN.md` for the full decision log.
|
|
102
|
+
|
|
103
|
+
## Troubleshooting
|
|
104
|
+
|
|
105
|
+
| Problem | Fix |
|
|
106
|
+
|---------|-----|
|
|
107
|
+
| "No browser session" | Run `python3 -m lattice ui login` |
|
|
108
|
+
| "Session expired" | Re-run `python3 -m lattice ui login` |
|
|
109
|
+
| Cloudflare blocks (403) | Session stale — re-login to get fresh `cf_clearance` cookies |
|
|
110
|
+
| Tool times out | Lattice page may be slow; try again |
|
|
111
|
+
| Create succeeds but objective not visible | Check the "All time" filter on `/goals` — it may default to current quarter |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|