vantage-peers-mcp 2.1.0 → 2.3.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/CHANGELOG.md +54 -0
- package/README.md +211 -0
- package/dist/server-http.js +102 -32
- package/dist/server.js +128 -16
- package/dist/src/auth.js +42 -1
- package/dist/src/tools.d.ts +8 -0
- package/dist/src/tools.js +20 -13
- package/package.json +7 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.3.0 — 2026-05-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `list_tasks`, `list_missions`, `list_tasks_by_mission`, `list_briefing_notes` now accept `fields=lite` for compact payloads.
|
|
7
|
+
- Status filters on `list_tasks`, `list_tasks_by_mission`, and `list_missions` now accept arrays and aliases:
|
|
8
|
+
- `status=["todo","in_progress"]` — multi-value array
|
|
9
|
+
- `status="open"` — expands to non-terminal statuses (tasks: todo+in_progress+review+blocked; missions: brainstorm+plan+execute+validate)
|
|
10
|
+
- `status="active"` — in_progress only on tasks; plan+execute on missions
|
|
11
|
+
- `status="all"` — no filter applied
|
|
12
|
+
|
|
13
|
+
### Backward compat
|
|
14
|
+
- Single-string status still accepted unchanged.
|
|
15
|
+
- Omitting `fields` defaults to `"full"` — existing callers unaffected.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2.2.0 — 2026-05-07
|
|
20
|
+
|
|
21
|
+
- 4 new fix-pattern tools: `create_fix_pattern`, `add_fix_attempt`, `validate_fix`, `link_issue_to_pattern`
|
|
22
|
+
- Detailed per-tool docs with arg tables and example calls in README
|
|
23
|
+
- New "Fix patterns cycle" section documenting the KB learning loop
|
|
24
|
+
- 41 new Zod input-validation unit tests for fix-pattern tools
|
|
25
|
+
|
|
26
|
+
## 2.1.1 — 2026-05-04
|
|
27
|
+
|
|
28
|
+
- Defense-in-depth `memoryIdSchema` validation for `create_briefing_note` and `update_briefing_note`
|
|
29
|
+
|
|
30
|
+
## 2.1.0 — 2026-04-25
|
|
31
|
+
|
|
32
|
+
- `update_briefing_note` MCP tool with RBAC
|
|
33
|
+
|
|
34
|
+
## 2.0.2 — 2026-04-14
|
|
35
|
+
|
|
36
|
+
- Added badges (npm version, downloads, license, tool count) to the published README
|
|
37
|
+
- Added Orchestrator Roles reference table including alpha, lambda, victor
|
|
38
|
+
- Added note that any custom lowercase role name is accepted
|
|
39
|
+
- Added `bugs` URL and additional keywords to `package.json`
|
|
40
|
+
|
|
41
|
+
## 2.0.1 — 2026-04-14
|
|
42
|
+
|
|
43
|
+
- Docstring fix in server.ts (minor)
|
|
44
|
+
|
|
45
|
+
## 2.0.0
|
|
46
|
+
|
|
47
|
+
- Type-safe `api.ts` export for cross-deployment calls (`vantage-peers-mcp/api`)
|
|
48
|
+
- Deploy key authentication guide
|
|
49
|
+
- Mission Templates category (1 tool: `update_mission_template`)
|
|
50
|
+
- Programmatic API section in README
|
|
51
|
+
|
|
52
|
+
## 1.x
|
|
53
|
+
|
|
54
|
+
- Initial public release with 82 MCP tools
|
package/README.md
CHANGED
|
@@ -50,6 +50,22 @@ Add to `~/.claude.json` or project `.claude/settings.json`:
|
|
|
50
50
|
}
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
## OAuth 2.1 DCR endpoints
|
|
54
|
+
|
|
55
|
+
VantagePeers ships a built-in OAuth 2.1 authorization server so Claude.ai web can connect via "Add custom integration" without any extra configuration.
|
|
56
|
+
|
|
57
|
+
| Method | Path | Description |
|
|
58
|
+
|--------|------|-------------|
|
|
59
|
+
| `GET` | `/.well-known/oauth-authorization-server` | Authorization Server Metadata (RFC 8414) — advertises supported grant types, endpoints, and capabilities |
|
|
60
|
+
| `GET` | `/.well-known/oauth-protected-resource` | Protected Resource Metadata (RFC 9728) — links back to the authorization server |
|
|
61
|
+
| `POST` | `/register` | Dynamic Client Registration (RFC 7591) — Claude.ai registers itself automatically on first connect |
|
|
62
|
+
| `GET` | `/authorize` | Authorization endpoint — redirects the user to grant access |
|
|
63
|
+
| `POST` | `/token` | Token endpoint — issues access tokens per OAuth 2.1 |
|
|
64
|
+
|
|
65
|
+
**RFCs implemented:** RFC 8414 (AS Metadata), RFC 9728 (Protected Resource Metadata), RFC 7591 (Dynamic Client Registration), OAuth 2.1 draft.
|
|
66
|
+
|
|
67
|
+
**Backward compatibility:** the `BEARER_SECRET_MASTER` env var still works unchanged. Claude Code and Claude Desktop users do not need to change anything — static bearer auth remains the default for those clients. OAuth 2.1 DCR is used exclusively when a client initiates the discovery flow (e.g. Claude.ai web).
|
|
68
|
+
|
|
53
69
|
## Environment variables
|
|
54
70
|
|
|
55
71
|
| Variable | Required | Description |
|
|
@@ -93,6 +109,104 @@ The server also reads `CONVEX_URL` from `.env.local` in the parent directory if
|
|
|
93
109
|
### Fix Patterns (5)
|
|
94
110
|
`create_fix_pattern`, `list_fix_patterns`, `add_fix_attempt`, `validate_fix`, `link_issue_to_pattern`
|
|
95
111
|
|
|
112
|
+
#### `create_fix_pattern`
|
|
113
|
+
Create a new fix pattern in the knowledge base. Documents a bug symptom, root cause, and optional validated fix. Agents search the KB before fixing to avoid repeating known mistakes.
|
|
114
|
+
|
|
115
|
+
| Arg | Type | Required | Description |
|
|
116
|
+
|-----|------|----------|-------------|
|
|
117
|
+
| `symptom` | string | yes | What the bug looks like — the user-visible problem |
|
|
118
|
+
| `rootCause` | string | yes | Why the bug happens — the underlying technical cause |
|
|
119
|
+
| `tags` | string or string[] | yes | Tags for categorization (e.g. `"react-hydration"`) |
|
|
120
|
+
| `stack` | string or string[] | yes | Tech stack involved (e.g. `"next.js"`, `"convex"`) |
|
|
121
|
+
| `sourceProject` | string | yes | Project where this was discovered |
|
|
122
|
+
| `createdBy` | string | yes | Orchestrator name (e.g. `"sigma"`) |
|
|
123
|
+
| `severity` | string | yes | `"critical"`, `"major"`, or `"minor"` |
|
|
124
|
+
| `validatedFix` | string | no | The fix that worked — set later if not yet known |
|
|
125
|
+
| `files` | string or string[] | no | Files involved in the fix |
|
|
126
|
+
| `linkedIssueIds` | string or string[] | no | VantagePeers issue IDs linked to this pattern |
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"tool": "create_fix_pattern",
|
|
132
|
+
"arguments": {
|
|
133
|
+
"symptom": "Convex subscription silently drops after 60s of inactivity",
|
|
134
|
+
"rootCause": "Missing keepAlive ping in useConvexQuery wrapper",
|
|
135
|
+
"tags": ["convex-subscription", "silent-failure"],
|
|
136
|
+
"stack": ["next.js", "convex"],
|
|
137
|
+
"sourceProject": "myreeldream",
|
|
138
|
+
"createdBy": "sigma",
|
|
139
|
+
"severity": "major",
|
|
140
|
+
"validatedFix": "Add 30s ping interval to the subscription hook"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `add_fix_attempt`
|
|
146
|
+
Log a fix attempt against an existing pattern. Documents what was tried, whether it worked, and why. If `worked: true` and the pattern has no `validatedFix`, auto-sets it.
|
|
147
|
+
|
|
148
|
+
| Arg | Type | Required | Description |
|
|
149
|
+
|-----|------|----------|-------------|
|
|
150
|
+
| `patternId` | string | yes | ID of the fix pattern |
|
|
151
|
+
| `description` | string | yes | What was tried — the fix approach |
|
|
152
|
+
| `worked` | boolean | yes | Whether this fix resolved the issue |
|
|
153
|
+
| `why` | string | yes | Why it worked or did not — the reasoning |
|
|
154
|
+
| `createdBy` | string | yes | Orchestrator name |
|
|
155
|
+
| `commit` | string | no | Git commit hash of this attempt |
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"tool": "add_fix_attempt",
|
|
161
|
+
"arguments": {
|
|
162
|
+
"patternId": "k5708d9xxwj81v92e0x3hwv36985g4d7",
|
|
163
|
+
"description": "Added 30s ping interval to useConvexQuery",
|
|
164
|
+
"worked": true,
|
|
165
|
+
"why": "Keeps the WebSocket connection alive past the server idle timeout",
|
|
166
|
+
"createdBy": "sigma",
|
|
167
|
+
"commit": "e866274"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### `validate_fix`
|
|
173
|
+
Promote a candidate fix to validated status on an existing pattern. Use after independently confirming the fix holds in production.
|
|
174
|
+
|
|
175
|
+
| Arg | Type | Required | Description |
|
|
176
|
+
|-----|------|----------|-------------|
|
|
177
|
+
| `patternId` | string | yes | ID of the fix pattern |
|
|
178
|
+
| `validatedFix` | string | yes | Description of the validated fix |
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"tool": "validate_fix",
|
|
184
|
+
"arguments": {
|
|
185
|
+
"patternId": "k5708d9xxwj81v92e0x3hwv36985g4d7",
|
|
186
|
+
"validatedFix": "30s ping interval in subscription hook — verified stable over 48h in production"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### `link_issue_to_pattern`
|
|
192
|
+
Link a VantagePeers issue to a fix pattern. Creates a bidirectional reference so the issue and pattern are discoverable from each other.
|
|
193
|
+
|
|
194
|
+
| Arg | Type | Required | Description |
|
|
195
|
+
|-----|------|----------|-------------|
|
|
196
|
+
| `patternId` | string | yes | ID of the fix pattern |
|
|
197
|
+
| `issueId` | string | yes | VantagePeers issue ID to link |
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"tool": "link_issue_to_pattern",
|
|
203
|
+
"arguments": {
|
|
204
|
+
"patternId": "k5708d9xxwj81v92e0x3hwv36985g4d7",
|
|
205
|
+
"issueId": "m97ewrrqczew67kc6at3a59e7985ea7h"
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
96
210
|
### Error Monitoring (2)
|
|
97
211
|
`list_errors`, `get_error`
|
|
98
212
|
|
|
@@ -114,6 +228,86 @@ The server also reads `CONVEX_URL` from `.env.local` in the parent directory if
|
|
|
114
228
|
### Session (1)
|
|
115
229
|
`set_summary`
|
|
116
230
|
|
|
231
|
+
## Compact payloads and status aliases (v2.3.0)
|
|
232
|
+
|
|
233
|
+
### `fields=lite` — reduced token payloads
|
|
234
|
+
|
|
235
|
+
`list_tasks`, `list_tasks_by_mission`, `list_missions`, and `list_briefing_notes` accept an optional `fields` parameter:
|
|
236
|
+
|
|
237
|
+
| Value | Behaviour |
|
|
238
|
+
|-------|-----------|
|
|
239
|
+
| `"full"` | Default. Returns the complete document (backward-compatible). |
|
|
240
|
+
| `"lite"` | Returns a compact projection — significantly fewer tokens. |
|
|
241
|
+
|
|
242
|
+
Lite projections per entity:
|
|
243
|
+
|
|
244
|
+
| Tool | Lite fields |
|
|
245
|
+
|------|------------|
|
|
246
|
+
| `list_tasks` / `list_tasks_by_mission` | `_id`, `_creationTime`, `title`, `status`, `priority`, `assignedTo`, `missionId` |
|
|
247
|
+
| `list_missions` | `_id`, `_creationTime`, `name`, `status`, `pilot`, `priority`, `project` |
|
|
248
|
+
| `list_briefing_notes` | `_id`, `_creationTime`, `topic`, `title`, `participants`, `createdBy` |
|
|
249
|
+
|
|
250
|
+
Example (tasks lite):
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"tool": "list_tasks",
|
|
254
|
+
"arguments": { "assignedTo": "sigma", "fields": "lite", "limit": 20 }
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
Returns:
|
|
258
|
+
```json
|
|
259
|
+
[
|
|
260
|
+
{ "_id": "k17e2r...", "title": "Prepare MCP v2.3.0", "status": "in_progress", "priority": "high", "assignedTo": "sigma", "missionId": "k572a..." }
|
|
261
|
+
]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### `status` arrays and aliases
|
|
265
|
+
|
|
266
|
+
`list_tasks`, `list_tasks_by_mission`, and `list_missions` now accept `status` as a single string, an array, or one of the aliases below.
|
|
267
|
+
|
|
268
|
+
#### Task status aliases
|
|
269
|
+
|
|
270
|
+
| Alias | Expands to |
|
|
271
|
+
|-------|-----------|
|
|
272
|
+
| `"open"` | `["todo", "in_progress", "review", "blocked"]` — everything except `done` |
|
|
273
|
+
| `"active"` | `["todo", "in_progress"]` |
|
|
274
|
+
| `"all"` | No filter — returns all statuses |
|
|
275
|
+
|
|
276
|
+
#### Mission status aliases
|
|
277
|
+
|
|
278
|
+
| Alias | Expands to |
|
|
279
|
+
|-------|-----------|
|
|
280
|
+
| `"open"` | `["brainstorm", "plan", "execute", "validate"]` — everything except `complete` |
|
|
281
|
+
| `"active"` | `["plan", "execute"]` |
|
|
282
|
+
| `"all"` | No filter — returns all statuses |
|
|
283
|
+
|
|
284
|
+
Examples:
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{ "tool": "list_tasks", "arguments": { "status": "open" } }
|
|
288
|
+
{ "tool": "list_tasks", "arguments": { "status": ["todo", "in_progress"] } }
|
|
289
|
+
{ "tool": "list_missions", "arguments": { "status": "active", "fields": "lite" } }
|
|
290
|
+
{ "tool": "list_tasks_by_mission", "arguments": { "missionId": "k572a...", "status": "all", "fields": "lite" } }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Single-string status values still work unchanged — fully backward-compatible.
|
|
294
|
+
|
|
295
|
+
## Fix patterns cycle
|
|
296
|
+
|
|
297
|
+
A fix pattern is a validated learning extracted from a resolved bug — symptom, root cause, and the fix that worked — stored in the VantagePeers knowledge base. Patterns accumulate across projects and agents so that the same bug is never debugged twice from scratch.
|
|
298
|
+
|
|
299
|
+
The cycle runs as follows:
|
|
300
|
+
|
|
301
|
+
1. **Agent encounters a bug.** Before touching any code, call `search_fix_patterns` with a plain-language description of the symptom. The KB returns ranked matches using semantic vector search.
|
|
302
|
+
2. **KB hit.** If a validated pattern is returned, apply the known fix directly. Log the reuse via `add_fix_attempt` (`worked: true`) so confidence scores stay current.
|
|
303
|
+
3. **KB miss.** If no pattern matches, the agent fixes the bug manually using standard debugging. Once resolved, the learning is captured immediately via `create_fix_pattern` — symptom, root cause, severity, stack, and the working fix.
|
|
304
|
+
4. **Validation.** After the fix holds in production (or after a second independent confirmation), call `validate_fix` to promote the pattern to validated status. This is the signal that downstream agents can trust the pattern without verification.
|
|
305
|
+
5. **Issue linkage.** Call `link_issue_to_pattern` to attach the VantagePeers issue ID to the pattern. This creates a bidirectional reference: the issue record points to the pattern, and the pattern's `linkedIssueIds` list points back.
|
|
306
|
+
|
|
307
|
+
The four tools that power this cycle are: `create_fix_pattern`, `add_fix_attempt`, `validate_fix`, and `link_issue_to_pattern`. The fifth tool, `search_fix_patterns`, is in the Search / RAG category and is the entry point agents should call first.
|
|
308
|
+
|
|
309
|
+
On the agent side, the `/capitalize-fix` skill and the `inject-fix-patterns` hook automate steps 3-5: the hook fires on task completion events and prompts the orchestrator to capture the learning before closing the task. The cycle is designed to be low-friction — one tool call per step, all via MCP, no `npx convex run` required.
|
|
310
|
+
|
|
117
311
|
## Programmatic API (TypeScript)
|
|
118
312
|
|
|
119
313
|
For external services that need type-safe access to VantagePeers functions:
|
|
@@ -206,6 +400,23 @@ All orchestrator names are open strings — any lowercase name is accepted. The
|
|
|
206
400
|
|
|
207
401
|
## Changelog
|
|
208
402
|
|
|
403
|
+
### 2.3.0 — 2026-05-26
|
|
404
|
+
- `list_tasks`, `list_missions`, `list_tasks_by_mission`, `list_briefing_notes` now accept `fields=lite` for compact payloads (less tokens).
|
|
405
|
+
- Status filters now accept arrays and aliases: `status=["todo","in_progress"]`, `status="open"` (expands to non-terminal), `status="active"` (in_progress only on tasks; plan+execute on missions), `status="all"` (no filter).
|
|
406
|
+
- Single-string status still accepted unchanged (backward-compatible).
|
|
407
|
+
|
|
408
|
+
### 2.2.0 — 2026-05-07
|
|
409
|
+
- 4 new fix-pattern tools: `create_fix_pattern`, `add_fix_attempt`, `validate_fix`, `link_issue_to_pattern`
|
|
410
|
+
- Detailed per-tool docs with arg tables and example calls in README
|
|
411
|
+
- New "Fix patterns cycle" section documenting the KB learning loop
|
|
412
|
+
- 41 new Zod input-validation unit tests for fix-pattern tools
|
|
413
|
+
|
|
414
|
+
### 2.1.1 — 2026-05-04
|
|
415
|
+
- Defense-in-depth `memoryIdSchema` validation for `create_briefing_note` and `update_briefing_note`
|
|
416
|
+
|
|
417
|
+
### 2.1.0 — 2026-04-25
|
|
418
|
+
- `update_briefing_note` MCP tool with RBAC
|
|
419
|
+
|
|
209
420
|
### 2.0.2 — 2026-04-14
|
|
210
421
|
- Added badges (npm version, downloads, license, tool count) to the published README
|
|
211
422
|
- Added Orchestrator Roles reference table including alpha, lambda, victor (Day 39 additions)
|
package/dist/server-http.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* PORT — HTTP port (default 3000)
|
|
25
25
|
* NODE_ENV — set to "production" on Railway
|
|
26
26
|
*/
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
27
28
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
28
29
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
29
30
|
import { ConvexHttpClient } from "convex/browser";
|
|
@@ -31,10 +32,19 @@ import { Hono } from "hono";
|
|
|
31
32
|
import { cors } from "hono/cors";
|
|
32
33
|
import { bearerAuthMiddleware, internalClient, masterOnlyMiddleware, sha256Base64Url, sha256Hex, } from "./src/auth.js";
|
|
33
34
|
import { registerTools } from "./src/tools.js";
|
|
35
|
+
let pkg;
|
|
36
|
+
try {
|
|
37
|
+
// Source mode: server-http.ts → ./package.json = mcp-server/package.json
|
|
38
|
+
pkg = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf-8"));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Dist mode: dist/server-http.js → ../package.json = mcp-server/package.json
|
|
42
|
+
pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
43
|
+
}
|
|
34
44
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
45
|
// Constants
|
|
36
46
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
-
const
|
|
47
|
+
const PUBLIC_BASE_URL_FALLBACK = process.env.PUBLIC_BASE_URL ??
|
|
38
48
|
"https://vantage-peers-production.up.railway.app";
|
|
39
49
|
const ACCESS_TOKEN_TTL_SECONDS = 3600; // 1 hour
|
|
40
50
|
const REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 3600; // 30 days
|
|
@@ -46,9 +56,34 @@ const DEFAULT_PUBLIC_DCR_PROFILE = "client-generic";
|
|
|
46
56
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
57
|
// Helpers
|
|
48
58
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Compute the issuer/base URL dynamically from the incoming request's Host
|
|
61
|
+
* header + protocol. Falls back to PUBLIC_BASE_URL env var when Host is absent
|
|
62
|
+
* (e.g., in curl smoke tests without a Host header).
|
|
63
|
+
*
|
|
64
|
+
* RFC 8414 §2: the issuer MUST be the URL the client uses to reach the server,
|
|
65
|
+
* so deriving it from the request is more correct than a hard-coded constant,
|
|
66
|
+
* especially when deployed behind a Railway/Cloudflare proxy that rewrites Host.
|
|
67
|
+
*/
|
|
68
|
+
function resolveIssuer(req) {
|
|
69
|
+
const host = req.headers.get("host");
|
|
70
|
+
if (host) {
|
|
71
|
+
// Use x-forwarded-proto when behind a reverse proxy; fall back to https.
|
|
72
|
+
const proto = req.headers.get("x-forwarded-proto") ??
|
|
73
|
+
(host.startsWith("localhost") || host.startsWith("127.")
|
|
74
|
+
? "http"
|
|
75
|
+
: "https");
|
|
76
|
+
return `${proto}://${host}`;
|
|
77
|
+
}
|
|
78
|
+
return PUBLIC_BASE_URL_FALLBACK;
|
|
79
|
+
}
|
|
49
80
|
function randomOpaqueToken() {
|
|
50
|
-
//
|
|
51
|
-
|
|
81
|
+
// 256-bit entropy via getRandomValues (32 bytes → 64 hex chars).
|
|
82
|
+
const bytes = new Uint8Array(32);
|
|
83
|
+
crypto.getRandomValues(bytes);
|
|
84
|
+
return Array.from(bytes)
|
|
85
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
86
|
+
.join("");
|
|
52
87
|
}
|
|
53
88
|
async function loadScopeProfile(profileId) {
|
|
54
89
|
return (await internalClient().query(
|
|
@@ -76,34 +111,65 @@ app.use("*", cors({
|
|
|
76
111
|
// OAuth 2.0 discovery (unauthenticated)
|
|
77
112
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
113
|
// RFC 9728 — OAuth 2.0 Protected Resource Metadata
|
|
79
|
-
app.get("/.well-known/oauth-protected-resource", (c) =>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
114
|
+
app.get("/.well-known/oauth-protected-resource", (c) => {
|
|
115
|
+
const issuer = resolveIssuer(c.req.raw);
|
|
116
|
+
return c.json({
|
|
117
|
+
resource: issuer,
|
|
118
|
+
authorization_servers: [issuer],
|
|
119
|
+
scopes_supported: ["mcp:full"],
|
|
120
|
+
});
|
|
121
|
+
});
|
|
85
122
|
// RFC 8414 — OAuth 2.0 Authorization Server Metadata
|
|
86
|
-
app.get("/.well-known/oauth-authorization-server", (c) =>
|
|
87
|
-
issuer
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
123
|
+
app.get("/.well-known/oauth-authorization-server", (c) => {
|
|
124
|
+
const issuer = resolveIssuer(c.req.raw);
|
|
125
|
+
return c.json({
|
|
126
|
+
issuer,
|
|
127
|
+
authorization_endpoint: `${issuer}/authorize`,
|
|
128
|
+
token_endpoint: `${issuer}/token`,
|
|
129
|
+
registration_endpoint: `${issuer}/register`,
|
|
130
|
+
response_types_supported: ["code"],
|
|
131
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
132
|
+
code_challenge_methods_supported: ["S256"],
|
|
133
|
+
token_endpoint_auth_methods_supported: [
|
|
134
|
+
"client_secret_basic",
|
|
135
|
+
"client_secret_post",
|
|
136
|
+
],
|
|
137
|
+
scopes_supported: ["mcp:full"],
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
const registerRateBuckets = new Map();
|
|
141
|
+
const REGISTER_RATE_LIMIT = 5;
|
|
142
|
+
const REGISTER_RATE_WINDOW_MS = 60_000;
|
|
143
|
+
function checkRegisterRateLimit(ip) {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const bucket = registerRateBuckets.get(ip);
|
|
146
|
+
if (!bucket || now - bucket.windowStart >= REGISTER_RATE_WINDOW_MS) {
|
|
147
|
+
registerRateBuckets.set(ip, { count: 1, windowStart: now });
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (bucket.count < REGISTER_RATE_LIMIT) {
|
|
151
|
+
bucket.count++;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
101
156
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
102
157
|
// RFC 7591 — Dynamic Client Registration
|
|
103
158
|
// Anonymous registrations get DEFAULT_PUBLIC_DCR_PROFILE ("client-generic").
|
|
104
159
|
// Pi must elevate the client via admin endpoint before real scopes are granted.
|
|
105
160
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
106
161
|
app.post("/register", async (c) => {
|
|
162
|
+
// S2: rate limit by IP — 5 req/min
|
|
163
|
+
const clientIp = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ??
|
|
164
|
+
c.req.header("x-real-ip") ??
|
|
165
|
+
"unknown";
|
|
166
|
+
if (!checkRegisterRateLimit(clientIp)) {
|
|
167
|
+
c.header("Retry-After", "60");
|
|
168
|
+
return c.json({
|
|
169
|
+
error: "too_many_requests",
|
|
170
|
+
error_description: "Rate limit exceeded. Max 5 registrations per minute per IP.",
|
|
171
|
+
}, 429);
|
|
172
|
+
}
|
|
107
173
|
let body = {};
|
|
108
174
|
try {
|
|
109
175
|
body = await c.req.json();
|
|
@@ -150,8 +216,8 @@ app.post("/register", async (c) => {
|
|
|
150
216
|
token_endpoint_auth_method: "client_secret_post",
|
|
151
217
|
grant_types: ["authorization_code", "refresh_token"],
|
|
152
218
|
response_types: ["code"],
|
|
153
|
-
|
|
154
|
-
|
|
219
|
+
// SC: standardized on mcp:full — consistent with well-known metadata
|
|
220
|
+
scope: "mcp:full",
|
|
155
221
|
}, 201);
|
|
156
222
|
});
|
|
157
223
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -164,7 +230,8 @@ app.get("/authorize", async (c) => {
|
|
|
164
230
|
const codeChallenge = q.code_challenge;
|
|
165
231
|
const codeChallengeMethod = q.code_challenge_method ?? "S256";
|
|
166
232
|
const state = q.state;
|
|
167
|
-
|
|
233
|
+
// SC: standardize scope — always mcp:full regardless of requested value
|
|
234
|
+
const scope = "mcp:full";
|
|
168
235
|
const responseType = q.response_type;
|
|
169
236
|
if (!clientId || !redirectUri || !codeChallenge) {
|
|
170
237
|
return c.json({
|
|
@@ -357,7 +424,8 @@ app.post("/token", async (c) => {
|
|
|
357
424
|
tokenHash: accessTokenHash,
|
|
358
425
|
clientId: record.clientId,
|
|
359
426
|
userId: record.userId,
|
|
360
|
-
|
|
427
|
+
// SC: standardized on mcp:full
|
|
428
|
+
scopes: ["mcp:full"],
|
|
361
429
|
scopeProfile: profile.profileId,
|
|
362
430
|
fromAllowList: profile.fromAllowList,
|
|
363
431
|
namespaceReadPrefixes: profile.namespaceReadPrefixes,
|
|
@@ -370,7 +438,8 @@ app.post("/token", async (c) => {
|
|
|
370
438
|
token_type: "Bearer",
|
|
371
439
|
expires_in: ACCESS_TOKEN_TTL_SECONDS,
|
|
372
440
|
refresh_token: refreshTokenRaw, // reused
|
|
373
|
-
|
|
441
|
+
// SC: standardized on mcp:full
|
|
442
|
+
scope: "mcp:full",
|
|
374
443
|
});
|
|
375
444
|
}
|
|
376
445
|
return c.json({ error: "unsupported_grant_type" }, 400);
|
|
@@ -381,9 +450,10 @@ app.post("/token", async (c) => {
|
|
|
381
450
|
app.get("/health", (c) => c.json({
|
|
382
451
|
status: "ok",
|
|
383
452
|
service: "vantage-peers-mcp-http",
|
|
384
|
-
version:
|
|
453
|
+
version: pkg.version,
|
|
385
454
|
transport: "streamable-http",
|
|
386
|
-
oauth: "
|
|
455
|
+
oauth: "supported",
|
|
456
|
+
scopes: ["mcp:full"],
|
|
387
457
|
}));
|
|
388
458
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
389
459
|
// Admin endpoints — master token only
|
|
@@ -490,7 +560,7 @@ app.all("/mcp", bearerAuthMiddleware(), async (c) => {
|
|
|
490
560
|
// Fresh McpServer per request — stateless mode, no session leakage
|
|
491
561
|
const server = new McpServer({
|
|
492
562
|
name: "vantage-peers",
|
|
493
|
-
version:
|
|
563
|
+
version: pkg.version,
|
|
494
564
|
});
|
|
495
565
|
registerTools(server, convex, oauthCtx);
|
|
496
566
|
const transport = new WebStandardStreamableHTTPServerTransport();
|
package/dist/server.js
CHANGED
|
@@ -433,7 +433,10 @@ server.tool("update_profile", "Create or update an orchestrator profile. Provide
|
|
|
433
433
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
434
434
|
server.tool("list_memories", "List active memories for a namespace, ordered newest first. " +
|
|
435
435
|
"Only returns isLatest=true memories (superseded memories are excluded by default). " +
|
|
436
|
-
"Use type to filter to a specific memory category."
|
|
436
|
+
"Use type to filter to a specific memory category. " +
|
|
437
|
+
"Returns { value: Memory[], continueCursor: string|null, isDone: boolean }. " +
|
|
438
|
+
"Pass paginationOpts.cursor from a previous response to fetch the next page. " +
|
|
439
|
+
"Without paginationOpts, returns ≤50 rows with continueCursor=null and isDone=true.", {
|
|
437
440
|
namespace: z
|
|
438
441
|
.string()
|
|
439
442
|
.describe("Namespace to list memories from — e.g. 'global', 'orchestrator/pi'"),
|
|
@@ -447,19 +450,36 @@ server.tool("list_memories", "List active memories for a namespace, ordered newe
|
|
|
447
450
|
.max(200)
|
|
448
451
|
.optional()
|
|
449
452
|
.default(20)
|
|
450
|
-
.describe("Maximum number of memories to return (default 20)"),
|
|
451
|
-
|
|
453
|
+
.describe("Maximum number of memories to return when paginationOpts is not provided (default 20, max 200)"),
|
|
454
|
+
paginationOpts: z
|
|
455
|
+
.object({
|
|
456
|
+
numItems: z
|
|
457
|
+
.number()
|
|
458
|
+
.int()
|
|
459
|
+
.min(1)
|
|
460
|
+
.max(200)
|
|
461
|
+
.describe("Number of items per page (max 200)"),
|
|
462
|
+
cursor: z
|
|
463
|
+
.union([z.string(), z.null()])
|
|
464
|
+
.describe("Cursor from a previous response continueCursor field, or null for the first page"),
|
|
465
|
+
})
|
|
466
|
+
.optional()
|
|
467
|
+
.describe("Optional cursor-based pagination. Pass { numItems, cursor: null } for the first page, " +
|
|
468
|
+
"then { numItems, cursor: <continueCursor from response> } for subsequent pages. " +
|
|
469
|
+
"When provided, isDone=false means more pages exist."),
|
|
470
|
+
}, async ({ namespace, type, limit, paginationOpts }) => {
|
|
452
471
|
try {
|
|
453
|
-
const
|
|
472
|
+
const result = await convex.query("memories:listMemories", {
|
|
454
473
|
namespace,
|
|
455
474
|
type,
|
|
456
475
|
limit: limit ?? 20,
|
|
476
|
+
paginationOpts,
|
|
457
477
|
});
|
|
458
478
|
return {
|
|
459
479
|
content: [
|
|
460
480
|
{
|
|
461
481
|
type: "text",
|
|
462
|
-
text: JSON.stringify(
|
|
482
|
+
text: JSON.stringify(result, null, 2),
|
|
463
483
|
},
|
|
464
484
|
],
|
|
465
485
|
};
|
|
@@ -831,14 +851,25 @@ server.tool("create_task", "Create a task in VantagePeers. Tasks are assigned to
|
|
|
831
851
|
// Tool: list_tasks
|
|
832
852
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
833
853
|
server.tool("list_tasks", "List tasks from VantagePeers with optional filters. " +
|
|
834
|
-
"Filter by assignee, instance, status, and/or project. Returns newest first."
|
|
854
|
+
"Filter by assignee, instance, status, and/or project. Returns newest first. " +
|
|
855
|
+
"status accepts a single value or an array, plus aliases: " +
|
|
856
|
+
"'open' (todo+in_progress+review+blocked), 'active' (todo+in_progress), 'all' (no filter). " +
|
|
857
|
+
"fields='lite' returns compact payloads ({_id,title,status,priority,assignedTo,missionId}) — fewer tokens.", {
|
|
835
858
|
assignedTo: assigneeSchema.optional().describe("Filter by assignee"),
|
|
836
859
|
assignedToInstance: z
|
|
837
860
|
.string()
|
|
838
861
|
.optional()
|
|
839
862
|
.describe("Filter by instance — e.g. 'pi-vps'. Returns only tasks assigned to that instance."),
|
|
840
|
-
status:
|
|
863
|
+
status: z
|
|
864
|
+
.union([taskStatusSchema, z.array(z.string()), z.string()])
|
|
865
|
+
.optional()
|
|
866
|
+
.describe("Filter by status. Single value, array, or alias: " +
|
|
867
|
+
"'open' (non-terminal), 'active' (in_progress only), 'all' (no filter)."),
|
|
841
868
|
project: z.string().optional().describe("Filter by project name"),
|
|
869
|
+
fields: z
|
|
870
|
+
.enum(["lite", "full"])
|
|
871
|
+
.optional()
|
|
872
|
+
.describe("'lite' = compact payload (less tokens), 'full' = default with all fields"),
|
|
842
873
|
limit: z
|
|
843
874
|
.number()
|
|
844
875
|
.int()
|
|
@@ -847,13 +878,14 @@ server.tool("list_tasks", "List tasks from VantagePeers with optional filters. "
|
|
|
847
878
|
.optional()
|
|
848
879
|
.default(50)
|
|
849
880
|
.describe("Maximum number of tasks to return (default 50)"),
|
|
850
|
-
}, async ({ assignedTo, assignedToInstance, status, project, limit }) => {
|
|
881
|
+
}, async ({ assignedTo, assignedToInstance, status, project, fields, limit }) => {
|
|
851
882
|
try {
|
|
852
883
|
const tasks = await convex.query("tasks:list", {
|
|
853
884
|
assignedTo,
|
|
854
885
|
assignedToInstance,
|
|
855
886
|
status,
|
|
856
887
|
project,
|
|
888
|
+
fields,
|
|
857
889
|
limit: limit ?? 50,
|
|
858
890
|
});
|
|
859
891
|
return {
|
|
@@ -1102,9 +1134,20 @@ server.tool("add_task_dependency", "Add a dependency to a task. The task cannot
|
|
|
1102
1134
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1103
1135
|
// Tool: list_tasks_by_mission
|
|
1104
1136
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1105
|
-
server.tool("list_tasks_by_mission", "List all tasks linked to a specific mission. Optionally filter by status."
|
|
1137
|
+
server.tool("list_tasks_by_mission", "List all tasks linked to a specific mission. Optionally filter by status. " +
|
|
1138
|
+
"status accepts a single value or an array, plus aliases: " +
|
|
1139
|
+
"'open' (todo+in_progress+review+blocked), 'active' (todo+in_progress), 'all' (no filter). " +
|
|
1140
|
+
"fields='lite' returns compact payloads ({_id,title,status,priority,assignedTo,missionId}) — fewer tokens.", {
|
|
1106
1141
|
missionId: z.string().describe("Convex document ID of the mission"),
|
|
1107
|
-
status:
|
|
1142
|
+
status: z
|
|
1143
|
+
.union([taskStatusSchema, z.array(z.string()), z.string()])
|
|
1144
|
+
.optional()
|
|
1145
|
+
.describe("Filter by task status. Single value, array, or alias: " +
|
|
1146
|
+
"'open' (non-terminal), 'active' (in_progress only), 'all' (no filter)."),
|
|
1147
|
+
fields: z
|
|
1148
|
+
.enum(["lite", "full"])
|
|
1149
|
+
.optional()
|
|
1150
|
+
.describe("'lite' = compact payload (less tokens), 'full' = default with all fields"),
|
|
1108
1151
|
limit: z
|
|
1109
1152
|
.number()
|
|
1110
1153
|
.int()
|
|
@@ -1113,11 +1156,12 @@ server.tool("list_tasks_by_mission", "List all tasks linked to a specific missio
|
|
|
1113
1156
|
.optional()
|
|
1114
1157
|
.default(50)
|
|
1115
1158
|
.describe("Maximum number of tasks to return (default 50)"),
|
|
1116
|
-
}, async ({ missionId, status, limit }) => {
|
|
1159
|
+
}, async ({ missionId, status, fields, limit }) => {
|
|
1117
1160
|
try {
|
|
1118
1161
|
const tasks = await convex.query("tasks:listByMission", {
|
|
1119
1162
|
missionId: missionId,
|
|
1120
1163
|
status,
|
|
1164
|
+
fields,
|
|
1121
1165
|
limit: limit ?? 50,
|
|
1122
1166
|
});
|
|
1123
1167
|
return {
|
|
@@ -1191,10 +1235,21 @@ server.tool("create_mission", "Create a mission in VantagePeers. Missions group
|
|
|
1191
1235
|
// Tool: list_missions
|
|
1192
1236
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1193
1237
|
server.tool("list_missions", "List missions from VantagePeers with optional filters. " +
|
|
1194
|
-
"Filter by project, pilot, and/or status. Returns newest first."
|
|
1238
|
+
"Filter by project, pilot, and/or status. Returns newest first. " +
|
|
1239
|
+
"status accepts a single value or an array, plus aliases: " +
|
|
1240
|
+
"'open' (brainstorm+plan+execute+validate), 'active' (plan+execute), 'all' (no filter). " +
|
|
1241
|
+
"fields='lite' returns compact payloads ({_id,name,status,pilot,priority,project}) — fewer tokens.", {
|
|
1195
1242
|
project: z.string().optional().describe("Filter by project name"),
|
|
1196
1243
|
pilot: creatorSchema.optional().describe("Filter by pilot orchestrator"),
|
|
1197
|
-
status:
|
|
1244
|
+
status: z
|
|
1245
|
+
.union([missionStatusSchema, z.array(z.string()), z.string()])
|
|
1246
|
+
.optional()
|
|
1247
|
+
.describe("Filter by status. Single value, array, or alias: " +
|
|
1248
|
+
"'open' (non-terminal), 'active' (plan+execute), 'all' (no filter)."),
|
|
1249
|
+
fields: z
|
|
1250
|
+
.enum(["lite", "full"])
|
|
1251
|
+
.optional()
|
|
1252
|
+
.describe("'lite' = compact payload (less tokens), 'full' = default with all fields"),
|
|
1198
1253
|
limit: z
|
|
1199
1254
|
.number()
|
|
1200
1255
|
.int()
|
|
@@ -1203,12 +1258,13 @@ server.tool("list_missions", "List missions from VantagePeers with optional filt
|
|
|
1203
1258
|
.optional()
|
|
1204
1259
|
.default(50)
|
|
1205
1260
|
.describe("Maximum number of missions to return (default 50)"),
|
|
1206
|
-
}, async ({ project, pilot, status, limit }) => {
|
|
1261
|
+
}, async ({ project, pilot, status, fields, limit }) => {
|
|
1207
1262
|
try {
|
|
1208
1263
|
const missions = await convex.query("missions:list", {
|
|
1209
1264
|
project,
|
|
1210
1265
|
pilot,
|
|
1211
1266
|
status,
|
|
1267
|
+
fields,
|
|
1212
1268
|
limit: limit ?? 50,
|
|
1213
1269
|
});
|
|
1214
1270
|
return {
|
|
@@ -1413,6 +1469,31 @@ server.tool("list_diaries", "List diary entries, optionally filtered by orchestr
|
|
|
1413
1469
|
}
|
|
1414
1470
|
});
|
|
1415
1471
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1472
|
+
// Tool: delete_diary
|
|
1473
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1474
|
+
server.tool("delete_diary", "Permanently delete a diary entry by ID. Only the owner (or system) can delete.", {
|
|
1475
|
+
diaryId: z.string().describe("Convex document ID of the diary entry to delete"),
|
|
1476
|
+
callerOrchestrator: creatorSchema.optional().describe("Optional RBAC — must be the owner or system"),
|
|
1477
|
+
}, async ({ diaryId, callerOrchestrator }) => {
|
|
1478
|
+
try {
|
|
1479
|
+
const result = await convex.mutation("diary:deleteDiary", {
|
|
1480
|
+
diaryId: diaryId,
|
|
1481
|
+
callerOrchestrator,
|
|
1482
|
+
});
|
|
1483
|
+
return {
|
|
1484
|
+
content: [
|
|
1485
|
+
{
|
|
1486
|
+
type: "text",
|
|
1487
|
+
text: JSON.stringify(result, null, 2),
|
|
1488
|
+
},
|
|
1489
|
+
],
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
catch (error) {
|
|
1493
|
+
return mcpError(error.message ?? String(error));
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1416
1497
|
// Tool: create_briefing_note
|
|
1417
1498
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1418
1499
|
server.tool("create_briefing_note", "Create a briefing note — a structured record of a topic discussion, with participants, " +
|
|
@@ -1511,11 +1592,16 @@ server.tool("update_briefing_note", "Update an existing briefing note. Partial-u
|
|
|
1511
1592
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1512
1593
|
// Tool: list_briefing_notes
|
|
1513
1594
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1514
|
-
server.tool("list_briefing_notes", "List briefing notes, optionally filtered by topic. Returns newest first."
|
|
1595
|
+
server.tool("list_briefing_notes", "List briefing notes, optionally filtered by topic. Returns newest first. " +
|
|
1596
|
+
"fields='lite' returns compact payloads ({_id,topic,title,participants,createdBy}) — fewer tokens.", {
|
|
1515
1597
|
topic: z
|
|
1516
1598
|
.string()
|
|
1517
1599
|
.optional()
|
|
1518
1600
|
.describe("Filter to a specific topic — omit for all"),
|
|
1601
|
+
fields: z
|
|
1602
|
+
.enum(["lite", "full"])
|
|
1603
|
+
.optional()
|
|
1604
|
+
.describe("'lite' = compact payload (less tokens), 'full' = default with all fields"),
|
|
1519
1605
|
limit: z
|
|
1520
1606
|
.number()
|
|
1521
1607
|
.int()
|
|
@@ -1524,10 +1610,11 @@ server.tool("list_briefing_notes", "List briefing notes, optionally filtered by
|
|
|
1524
1610
|
.optional()
|
|
1525
1611
|
.default(20)
|
|
1526
1612
|
.describe("Maximum notes to return (default 20)"),
|
|
1527
|
-
}, async ({ topic, limit }) => {
|
|
1613
|
+
}, async ({ topic, fields, limit }) => {
|
|
1528
1614
|
try {
|
|
1529
1615
|
const notes = await convex.query("briefingNotes:list", {
|
|
1530
1616
|
topic,
|
|
1617
|
+
fields,
|
|
1531
1618
|
limit: limit ?? 20,
|
|
1532
1619
|
});
|
|
1533
1620
|
return {
|
|
@@ -1544,6 +1631,31 @@ server.tool("list_briefing_notes", "List briefing notes, optionally filtered by
|
|
|
1544
1631
|
}
|
|
1545
1632
|
});
|
|
1546
1633
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1634
|
+
// Tool: delete_briefing_note
|
|
1635
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1636
|
+
server.tool("delete_briefing_note", "Permanently delete a briefing note by ID. Only the creator (or system) can delete.", {
|
|
1637
|
+
noteId: z.string().describe("Convex document ID of the briefing note to delete"),
|
|
1638
|
+
callerOrchestrator: creatorSchema.optional().describe("Optional RBAC — must be creator or system"),
|
|
1639
|
+
}, async ({ noteId, callerOrchestrator }) => {
|
|
1640
|
+
try {
|
|
1641
|
+
const result = await convex.mutation("briefingNotes:deleteBriefingNote", {
|
|
1642
|
+
noteId: noteId,
|
|
1643
|
+
callerOrchestrator,
|
|
1644
|
+
});
|
|
1645
|
+
return {
|
|
1646
|
+
content: [
|
|
1647
|
+
{
|
|
1648
|
+
type: "text",
|
|
1649
|
+
text: JSON.stringify(result, null, 2),
|
|
1650
|
+
},
|
|
1651
|
+
],
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
catch (error) {
|
|
1655
|
+
return mcpError(error.message ?? String(error));
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1547
1659
|
// Tool: register_component
|
|
1548
1660
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1549
1661
|
server.tool("register_component", "Register or update a component (agent, skill, hook, or plugin) in the registry. " +
|
package/dist/src/auth.js
CHANGED
|
@@ -212,7 +212,48 @@ export function bearerAuthMiddleware() {
|
|
|
212
212
|
await next();
|
|
213
213
|
return;
|
|
214
214
|
}
|
|
215
|
-
// ── (3)
|
|
215
|
+
// ── (3) DCR OAuth token — check oauthTokens via oauthDcr:validateAccessToken
|
|
216
|
+
// Uses raw token (not hashed) — the DCR table stores tokens in plaintext.
|
|
217
|
+
// This path handles Claude.ai clients registered via POST /register.
|
|
218
|
+
let dcrResult = null;
|
|
219
|
+
try {
|
|
220
|
+
dcrResult = (await internalClient().query(
|
|
221
|
+
// biome-ignore lint/suspicious/noExplicitAny: Convex string API
|
|
222
|
+
"oauthDcr:validateAccessToken", { accessToken: token }));
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
226
|
+
console.warn("[auth] DCR OAuth lookup skipped:", message);
|
|
227
|
+
}
|
|
228
|
+
if (dcrResult?.valid === true) {
|
|
229
|
+
const internalUrl = process.env.CONVEX_URL_INTERNAL;
|
|
230
|
+
if (!internalUrl) {
|
|
231
|
+
console.error("[auth] CONVEX_URL_INTERNAL not set — cannot route DCR OAuth token");
|
|
232
|
+
return c.json({ error: "Server misconfigured: internal deployment URL missing" }, 500);
|
|
233
|
+
}
|
|
234
|
+
// Map DCR single-scope string → OAuthContext fields.
|
|
235
|
+
// DCR tokens always carry "mcp:full" which maps to full access.
|
|
236
|
+
const scopes = dcrResult.scope.split(/\s+/).filter(Boolean);
|
|
237
|
+
const isFull = scopes.includes("mcp:full");
|
|
238
|
+
c.set("tenant", {
|
|
239
|
+
tenantName: `dcr:${dcrResult.clientId}`,
|
|
240
|
+
convexUrl: internalUrl,
|
|
241
|
+
});
|
|
242
|
+
c.set("oauthContext", {
|
|
243
|
+
clientId: dcrResult.clientId,
|
|
244
|
+
userId: dcrResult.clientId,
|
|
245
|
+
scopes,
|
|
246
|
+
scopeProfile: isFull ? "master" : "client-generic",
|
|
247
|
+
fromAllowList: isFull ? ["*"] : [],
|
|
248
|
+
namespaceReadPrefixes: isFull ? ["*"] : [],
|
|
249
|
+
namespaceWritePrefixes: isFull ? ["*"] : [],
|
|
250
|
+
expiresAt: dcrResult.expiresAt,
|
|
251
|
+
isMaster: false,
|
|
252
|
+
});
|
|
253
|
+
await next();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// ── (4) Legacy internal bearer — mcpTenants table ───────────────────────
|
|
216
257
|
let tenant;
|
|
217
258
|
try {
|
|
218
259
|
tenant = (await internalClient().query(
|
package/dist/src/tools.d.ts
CHANGED
|
@@ -27,6 +27,14 @@ export declare function assertContentSize(content: string, toolName: string): nu
|
|
|
27
27
|
*/
|
|
28
28
|
export declare const convexIdPattern: RegExp;
|
|
29
29
|
export declare const receiptIdSchema: z.ZodString;
|
|
30
|
+
export declare const memoryIdSchema: z.ZodString;
|
|
31
|
+
export declare const creatorSchema: z.ZodString;
|
|
32
|
+
export declare const severitySchema: z.ZodEnum<{
|
|
33
|
+
critical: "critical";
|
|
34
|
+
major: "major";
|
|
35
|
+
minor: "minor";
|
|
36
|
+
}>;
|
|
37
|
+
export declare const flexArray: z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodString]>;
|
|
30
38
|
export declare const updateBriefingNoteDescription: string;
|
|
31
39
|
export declare const updateBriefingNoteSchema: z.ZodObject<{
|
|
32
40
|
noteId: z.ZodString;
|
package/dist/src/tools.js
CHANGED
|
@@ -51,17 +51,20 @@ export const convexIdPattern = /^[a-z0-9]{32}$/;
|
|
|
51
51
|
export const receiptIdSchema = z
|
|
52
52
|
.string()
|
|
53
53
|
.regex(convexIdPattern, "receiptId must be a 32-char lowercase alphanumeric Convex ID");
|
|
54
|
+
export const memoryIdSchema = z
|
|
55
|
+
.string()
|
|
56
|
+
.regex(convexIdPattern, "Invalid memory ID format (expected 32-char Convex ID)");
|
|
54
57
|
const memoryTypeSchema = z
|
|
55
58
|
.enum(["user", "feedback", "project", "reference", "episode"])
|
|
56
59
|
.describe("Memory classification type");
|
|
57
|
-
const creatorSchema = z
|
|
60
|
+
export const creatorSchema = z
|
|
58
61
|
.string()
|
|
59
|
-
.describe("Orchestrator role name (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, laurent, or any custom client role (lowercase string)). " +
|
|
62
|
+
.describe("Orchestrator role name (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, epsilon, omicron, upsilon, laurent, or any custom client role (lowercase string)). " +
|
|
60
63
|
"New internal orchestrators use Greek letters (lowercase); external client orchestrators use free lowercase strings.");
|
|
61
|
-
const severitySchema = z
|
|
64
|
+
export const severitySchema = z
|
|
62
65
|
.enum(["critical", "major", "minor"])
|
|
63
66
|
.describe("Episode severity — critical = cross-orchestrator lesson");
|
|
64
|
-
const flexArray = z.union([z.array(z.string()), z.string()]);
|
|
67
|
+
export const flexArray = z.union([z.array(z.string()), z.string()]);
|
|
65
68
|
const flexArrayOptional = flexArray.optional();
|
|
66
69
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
70
|
// update_briefing_note — Zod schema + description
|
|
@@ -96,13 +99,13 @@ export const updateBriefingNoteSchema = z.object({
|
|
|
96
99
|
.optional()
|
|
97
100
|
.describe("Optional new decisions array — full replace, not append"),
|
|
98
101
|
linkedMemoryIds: z
|
|
99
|
-
.array(
|
|
102
|
+
.array(memoryIdSchema)
|
|
100
103
|
.optional()
|
|
101
|
-
.describe("Optional new linkedMemoryIds array — full replace, not append. Each ID must point to memories table."),
|
|
104
|
+
.describe("Optional new linkedMemoryIds array — full replace, not append. Each ID must point to memories table, NOT briefingNotes or any other table."),
|
|
102
105
|
});
|
|
103
106
|
const assigneeSchema = z
|
|
104
107
|
.string()
|
|
105
|
-
.describe("Orchestrator to assign to (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, laurent, or any custom client role (lowercase string)). " +
|
|
108
|
+
.describe("Orchestrator to assign to (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, epsilon, omicron, upsilon, laurent, or any custom client role (lowercase string)). " +
|
|
106
109
|
"New internal orchestrators use Greek letters (lowercase); external client orchestrators use free lowercase strings.");
|
|
107
110
|
const prioritySchema = z
|
|
108
111
|
.enum(["urgent", "high", "medium", "low"])
|
|
@@ -612,7 +615,7 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
612
615
|
server.tool("send_message", "Send a message to one, many, or all orchestrators. " +
|
|
613
616
|
"channel: 'broadcast' = all, 'tau' = role DM, 'pi-vps' = instance DM, 'tau,phi' = multi. " +
|
|
614
617
|
"Creates message + one receipt per recipient. Replaces claude-peers send_message.", {
|
|
615
|
-
from: creatorSchema.describe("Sender role (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, or any custom role)"),
|
|
618
|
+
from: creatorSchema.describe("Sender role (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, epsilon, omicron, upsilon, or any custom role)"),
|
|
616
619
|
fromInstanceId: z
|
|
617
620
|
.string()
|
|
618
621
|
.optional()
|
|
@@ -670,7 +673,7 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
670
673
|
server.tool("check_messages", "Check for unread messages. Returns messages with receiptIds for marking as read. " +
|
|
671
674
|
"If recipientInstanceId is provided, returns instance-targeted + role-level messages. " +
|
|
672
675
|
"Replaces claude-peers check_messages.", {
|
|
673
|
-
recipient: creatorSchema.describe("Orchestrator role (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, or any custom role)"),
|
|
676
|
+
recipient: creatorSchema.describe("Orchestrator role (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, epsilon, omicron, upsilon, or any custom role)"),
|
|
674
677
|
recipientInstanceId: z
|
|
675
678
|
.string()
|
|
676
679
|
.optional()
|
|
@@ -1626,7 +1629,8 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
1626
1629
|
});
|
|
1627
1630
|
// ── create_briefing_note ────────────────────────────────────────────────────
|
|
1628
1631
|
server.tool("create_briefing_note", "Create a briefing note — a structured record of a topic discussion, with participants, " +
|
|
1629
|
-
"content, optional decisions, and optional links to existing memories."
|
|
1632
|
+
"content, optional decisions, and optional links to existing memories. " +
|
|
1633
|
+
"linkedMemoryIds MUST contain IDs from the memories table only — NOT briefingNotes IDs or IDs from any other table.", {
|
|
1630
1634
|
title: z.string().describe("Briefing note title"),
|
|
1631
1635
|
topic: z
|
|
1632
1636
|
.string()
|
|
@@ -1636,7 +1640,10 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
1636
1640
|
.describe("Who participated — e.g. ['pi', 'sigma'] or 'pi'"),
|
|
1637
1641
|
content: z.string().describe("Full briefing content"),
|
|
1638
1642
|
decisions: flexArrayOptional.describe("Decisions made during the briefing"),
|
|
1639
|
-
linkedMemoryIds:
|
|
1643
|
+
linkedMemoryIds: z
|
|
1644
|
+
.array(memoryIdSchema)
|
|
1645
|
+
.optional()
|
|
1646
|
+
.describe("Convex document IDs of related memories — each must be a 32-char ID from the memories table, NOT briefingNotes or any other table"),
|
|
1640
1647
|
createdBy: creatorSchema,
|
|
1641
1648
|
}, async ({ title, topic, participants, content, decisions, linkedMemoryIds, createdBy, }) => {
|
|
1642
1649
|
let contentBytes = 0;
|
|
@@ -2529,7 +2536,7 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
2529
2536
|
server.tool("add_repo_mapping", "Add or update a GitHub repo → orchestrator mapping. Used by the webhook pipeline to route GitHub events to the right orchestrator.", {
|
|
2530
2537
|
repo: z
|
|
2531
2538
|
.string()
|
|
2532
|
-
.describe("Full repo name — e.g. '
|
|
2539
|
+
.describe("Full repo name — e.g. 'vantageos-agency/vantage-peers'"),
|
|
2533
2540
|
orchestrator: z
|
|
2534
2541
|
.string()
|
|
2535
2542
|
.describe("Target orchestrator — e.g. 'sigma', 'omega', 'tau'"),
|
|
@@ -2583,7 +2590,7 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
2583
2590
|
server.tool("remove_repo_mapping", "Remove a GitHub repo mapping by repo name. Stops routing webhook events for this repo.", {
|
|
2584
2591
|
repo: z
|
|
2585
2592
|
.string()
|
|
2586
|
-
.describe("Full repo name to remove — e.g. '
|
|
2593
|
+
.describe("Full repo name to remove — e.g. 'vantageos-agency/vantage-peers'"),
|
|
2587
2594
|
}, async ({ repo }) => {
|
|
2588
2595
|
try {
|
|
2589
2596
|
const result = await convex.mutation("githubRepoMapping:remove", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vantage-peers-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "MCP server for VantagePeers — shared memory, messaging, and task coordination for AI agent teams",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/server.js",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist/",
|
|
19
|
-
"README.md"
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
23
|
"generate:api": "cd .. && npx convex-helpers ts-api-spec --prod --output-file mcp-server/api.ts",
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
"author": "ElPi Corp",
|
|
57
58
|
"license": "FSL-1.1-Apache-2.0",
|
|
58
59
|
"dependencies": {
|
|
59
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
60
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
60
61
|
"convex": "^1.34.0",
|
|
61
62
|
"dotenv": "^17.4.2",
|
|
62
63
|
"zod": "^4.3.6"
|
|
@@ -68,6 +69,9 @@
|
|
|
68
69
|
"typescript": "^5.9.3",
|
|
69
70
|
"@types/node": "^24.12.2"
|
|
70
71
|
},
|
|
72
|
+
"overrides": {
|
|
73
|
+
"path-to-regexp": "^8.4.0"
|
|
74
|
+
},
|
|
71
75
|
"engines": {
|
|
72
76
|
"node": ">=18"
|
|
73
77
|
},
|