vantage-peers-mcp 2.3.4 → 2.4.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 CHANGED
@@ -1,5 +1,125 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.4.0] — 2026-05-28 — M3 iframeEmbedSessions + __VP_TOOL_RESULT__ stream marker + ack-checklist
4
+
5
+ **Mission instance** : `sigma-vantage-peers-mcp-gui-iframe-embed-v1` (k5730xct6rvrwkvxhy5t5js12d87jwfw).
6
+ **Pi sign-off** : PI_AUTHORIZED_TASK_ID=`k1793m1qgn0zaay6r87dhvsh7187kwya` (PROD-DEPLOY-AUTHORIZED).
7
+ **Eta sign-off** : ETA_APPROVED_TASK_ID=`k171ep964sxabbrgmb21fk9axd87ka1n` at commit `338a7b9e6130ce69dc5fe7f3e2e9ecc4648b4f6a` (Day 79 SHA-pinned).
8
+ **Merge** : PR #545 squash `f509c8d92f0b142bc063a0e9dd070e1993cc729b`.
9
+
10
+ M3 delivers the session registry and stream-marker protocol that connects the VP MCP server
11
+ to the Gen UI iframe bridge. All marker emission is gated behind `VP_EMIT_UI_MARKERS=1`
12
+ so production behaviour is unchanged until the bridge is deployed.
13
+
14
+ ### Convex schema — `iframeEmbedSessions` table
15
+
16
+ NEW table `iframeEmbedSessions` in `convex/schema.ts` :
17
+ - Fields : `sessionId` (string), `tenantId` (optional string), `origin` (string),
18
+ `userId` (optional string), `createdAt` (number), `lastSeenAt` (number),
19
+ `expiresAt` (number), `revoked` (boolean).
20
+ - Indexes : `by_session_id` on `["sessionId"]`, `by_origin_expires` on `["origin", "expiresAt"]`.
21
+
22
+ NEW `convex/iframeEmbedSessions.ts` — 4 operations :
23
+ - `createSession` mutation — inserts a new session row.
24
+ - `getSession` query — returns session or null (null for expired / revoked).
25
+ - `touchSession` mutation — bumps `lastSeenAt` to now; returns bool.
26
+ - `revokeSession` mutation — sets `revoked=true`; returns bool.
27
+
28
+ ### Stream marker — `mcp-server/src/ui-resources/stream-marker.ts`
29
+
30
+ NEW `MARKER_START = "__VP_TOOL_RESULT__"`, `MARKER_END = "__END__"`.
31
+
32
+ NEW `wrapToolResult(payload: VpToolResult): string` :
33
+ - Validates via `VpToolResultSchema`, throws `TypeError` on schema failure.
34
+ - Returns `__VP_TOOL_RESULT__<json>__END__`.
35
+
36
+ NEW `parseToolResult(text: string): VpToolResult | null` :
37
+ - Extracts marker substring (handles bare, embedded, surrounding text).
38
+ - Returns validated `VpToolResult` or null on any failure (no-throw contract).
39
+
40
+ ### MCP tools — marker emission gated by `VP_EMIT_UI_MARKERS=1`
41
+
42
+ `mcp-server/src/tools.ts` — 6 tools now append `wrapToolResult(...)` after the JSON payload
43
+ when `VP_EMIT_UI_MARKERS=1` (default OFF) :
44
+
45
+ | Tool | kind |
46
+ |-----------------------|--------------------|
47
+ | `list_tasks` | `tasks-table` |
48
+ | `list_messages` | `messages-feed` |
49
+ | `get_diary` | `diary-entry` |
50
+ | `list_missions` | `mission-timeline` |
51
+ | `list_briefing_notes` | `briefing-note` |
52
+ | `list_memories` | `memory-quote` |
53
+
54
+ Change is surgical — existing return shape is preserved; marker is appended as a new line.
55
+
56
+ ### Ack checklist
57
+
58
+ NEW `docs/M3-ACK-CHECKLIST.md` — bilingual FR/EN post-deploy verification checklist
59
+ for Marie + Ismaël. Covers: package install, primitive reads, Shadow DOM scoping,
60
+ stream marker emit + parse, bilingual spot check, WCAG AA (contrast + role attrs),
61
+ default-OFF guard.
62
+
63
+ ### Tests
64
+
65
+ 15+ new vitest cases (≥264 total after M3, baseline 253 after M2) :
66
+ - `mcp-server/src/__tests__/m3-stream-marker.test.ts` — 14 cases:
67
+ `wrapToolResult` ×6 valid kinds, ×2 throws on invalid, `parseToolResult` roundtrip,
68
+ non-marker text ×2, embedded text, malformed JSON ×2, schema rejects unknown kind ×2.
69
+ - `convex/iframeEmbedSessions.test.ts` — 7 cases:
70
+ create+get, optional fields, getSession unknown, expired session null,
71
+ touchSession updates lastSeenAt, touchSession unknown false,
72
+ revokeSession marks revoked (getSession null), revokeSession unknown false.
73
+
74
+ 0 regression on M1+M2 suites (253/253 baseline).
75
+
76
+ ---
77
+
78
+ ## [Unreleased] — M1 SEP-1865 ui:// resources backend + M2 primitives + Zod schemas
79
+
80
+ **Mission instance** : `sigma-vantage-peers-mcp-gui-iframe-embed-v1` (k5730xct6rvrwkvxhy5t5js12d87jwfw).
81
+ **Template VR consumed** : `gui-iframe-embed-v1` v1.0.0 (jx7bzk0x1086tgwgj2zrssk2pn87k1ga).
82
+
83
+ M1 Foundation (adapted MCP-pure paradigm per Pi arbitrage Day 84) :
84
+ - NEW `mcp-server/src/ui-resources/index.ts` : URI parser `ui://vp/v1/<primitive>?<query>` + primitive registry + handler factory.
85
+ - NEW `mcp-server/src/ui-resources/primitives/tasks-table.ts` : M1 MVP primitive returning HTML inline (Shadow DOM scoped CSS) — WCAG AA + bilingual FR+EN.
86
+ - `mcp-server/server-http.ts` : wired `ListResourcesRequestSchema` + `ReadResourceRequestSchema` MCP handlers on the existing McpServer instance.
87
+
88
+ Tests : 14 new vitest cases (`src/__tests__/ui-resources-sep-1865.test.ts`) — URI parsing, primitive registry, render variants (empty, populated, FR), backend arg forwarding, XSS escape, error fallback, limit clamping, unknown primitive rejection. 0 regression on existing suites.
89
+
90
+ ### M2 — Resolve 5 Gaps + Bearer sha256 hardening (adapted MCP-pure paradigm)
91
+
92
+ 5 new ui:// primitives :
93
+ - `messages-feed` (`messages:listMessages` backend — channel filter applied client-side)
94
+ - `diary-entry` (`diary:get` single-entry + `diary:list` multi-entry backend)
95
+ - `mission-timeline` (`missions:list` backend with fields=lite)
96
+ - `briefing-note` (`briefingNotes:get` by noteId OR `briefingNotes:list` by topic backend)
97
+ - `memory-quote` (`memories:listMemories` backend — supports both plain-array and paginated result shapes)
98
+
99
+ Zod discriminated union schemas : `mcp-server/src/ui-resources/schemas.ts` exports `VpTaskPayloadSchema` + `VpMessagePayloadSchema` + `VpDiaryEntryPayloadSchema` + `VpMissionPayloadSchema` + `VpBriefingNotePayloadSchema` + `VpMemoryPayloadSchema` + `VpToolResultSchema` (discriminated union by `kind`). Cross-fleet ready for Mu vantage-bridge sidepanel S3 consumer.
100
+
101
+ Bearer sha256 validation : Already in place since v2.3.4 DCR security fix. `mcp-server/src/auth.ts` line 275 calls `sha256Hex(token)` before every Convex lookup (layers 2 and 4). Raw token never reaches Convex. No further hardening needed in M2.
102
+
103
+ Tests : 42 new vitest cases in `src/__tests__/ui-resources-m2-primitives.test.ts` (target was ≥22). Covers : PRIMITIVES registry (6 entries), each of 5 new primitives (empty + populated + FR labels + XSS escape + error fallback = 5 cases each), Zod schema roundtrip (VpToolResultSchema all 6 variants accepted, malformed rejected, individual payload schema validations). 0 regression on M1 17 cases + 194 other MCP tests (253/253 total).
104
+
105
+ M3 next : Registry json-render + `__VP_TOOL_RESULT__<json>` stream marker + smoke E2E + ack-checklist + PI-SIGNED Convex prod deploy + visual ack Marie/Ismaël.
106
+
107
+ ## v2.3.5 — 2026-05-28
108
+
109
+ **Critical hotfix** — v2.3.3 (PR #539) shipped the backend filters `createdBy` + `updatedSince` and the Zod schema exports but did NOT wire those params into the 4 list MCP tool args blocks. Pi pull-cycle quickstart `list_tasks createdBy="pi" status="review" fields="lite"` was silently dropping `createdBy` at the MCP boundary and returning all visible tasks. Auto-clamp safeguard (Day 83) also could not trigger because Zod `.default(50)` / `.default(20)` on `limit` overrode the absent-value signal before it reached the backend.
110
+
111
+ Fixes:
112
+ - `mcp-server/src/tools.ts` : 4 list tools now expose `createdBy` (`list_tasks` + `list_tasks_by_mission` only — `list_missions` + `list_briefing_notes` do not accept it backend-side) and `updatedSince` (all 4).
113
+ - Removed `.default(50)` (3 tools) and `.default(20)` (1 tool) on `limit` so absent value reaches the backend, enabling the v2.3.3 auto-clamp safeguard.
114
+
115
+ Tests : 8 new boundary-forwarding cases (`src/__tests__/list-queries-v2.3.5-wire-createdby-updatedsince.test.ts`) — verify MCP layer actually forwards new params to `convex.query` instead of dropping them. 0 regression on existing suites.
116
+
117
+ Detection : Vantage-Bridge architecture review Sigma scope Day 84 — direct `grep`/`sed` inspection of `tools.ts` confirmed the gap. Backend already correct since v2.3.3 (`convex/tasks.ts:354-357`).
118
+
119
+ Fix-pattern (Day 84 capitalize) : when adding a new param across backend + MCP wrapper, the test suite MUST cover not only schema validation but also the tool-handler→convex.query forwarding boundary. Schema-only tests passed cleanly in v2.3.3 while the actual feature was broken in prod.
120
+
121
+ VP task : `k177tsvdxzase5sjy2qm9fdvp187kbwr`. Predecessor v2.3.3 PR #539 (`k1796s5j6jfkvkx0tn5n926ftd87jx9p`).
122
+
3
123
  ## v2.3.4 — 2026-05-28
4
124
 
5
125
  **Security fix** — DCR (Dynamic Client Registration) self-registration now defaults to tenant-scope only. Master scope requires explicit admin authorization (`ADMIN_DCR_TOKEN` / `BEARER_SECRET_MASTER` env var). Closes beta blocker for Marie/Iris RH onboarding identified in VP Cloud audit Day 84.
@@ -13,6 +133,28 @@ Tests: 5 new Convex security tests (`convex/oauth-dcr-security.test.ts`) + 5 new
13
133
 
14
134
  VP task: k17218rvqyncs1v6rwj3qdzfsn87jj4n. Beta unblock chain: DCR fix → 5 quick wins onboarding (seed-profiles + marie-iris-rh client + README VP Cloud + runbook + email).
15
135
 
136
+ ## v2.3.3 — 2026-05-28
137
+
138
+ **Follow-up to v2.3.2 (Day 84 scope élargi)** — Extend list queries with `createdBy` + `updatedSince` filters + auto-clamp safeguard.
139
+
140
+ Backend (Convex) :
141
+ - `tasks.list` + `tasks.listByMission` : + `createdBy` (filter by task creator) + `updatedSince` (Unix ms window) + auto-clamp limit=30 when `fields="full"` and no explicit limit
142
+ - `missions.list` : + `updatedSince` + auto-clamp (30)
143
+ - `briefingNotes.list` : + `updatedSince` + auto-clamp (15 when fields=full)
144
+
145
+ MCP wrapper :
146
+ - 4 list tools forward the new params
147
+ - New export `updatedSinceSchema` (positive integer ms)
148
+ - `limit` `.default()` removed on the 4 list tools so absent limit flows to backend → enables auto-clamp
149
+
150
+ Tests : 15 new MCP schema cases (`src/__tests__/list-queries-v2.3.3-createdby-updatedsince.test.ts`) + 6 new Convex round-trip cases.
151
+
152
+ Pi pull cycle unblocked : `list_tasks createdBy="pi" status="review" fields="lite"` returns only Pi-dispatched tasks recently moved to review, payload 5-10× smaller.
153
+
154
+ Cap fleet : 0 overflow tolérance future (auto-clamp).
155
+
156
+ VP task: `k1796s5j6jfkvkx0tn5n926ftd87jx9p`. Successor of `k17e09ng1tf217n93z9m4tr0mx87hfe0` (v2.3.2 PR #537).
157
+
16
158
  ## v2.3.2 — 2026-05-28
17
159
 
18
160
  **Hotfix** — Expose `fields="lite"` + `status` array/aliases in MCP tool schemas (Day 82 sprint gap).
@@ -25,13 +25,14 @@
25
25
  * NODE_ENV — set to "production" on Railway
26
26
  */
27
27
  import { readFileSync } from "node:fs";
28
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
28
+ import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
29
29
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
30
30
  import { ConvexHttpClient } from "convex/browser";
31
31
  import { Hono } from "hono";
32
32
  import { cors } from "hono/cors";
33
33
  import { bearerAuthMiddleware, internalClient, masterOnlyMiddleware, sha256Base64Url, sha256Hex, } from "./src/auth.js";
34
34
  import { registerTools } from "./src/tools.js";
35
+ import { listUiResources, readUiResource } from "./src/ui-resources/index.js";
35
36
  let pkg;
36
37
  try {
37
38
  // Source mode: server-http.ts → ./package.json = mcp-server/package.json
@@ -563,6 +564,31 @@ app.all("/mcp", bearerAuthMiddleware(), async (c) => {
563
564
  version: pkg.version,
564
565
  });
565
566
  registerTools(server, convex, oauthCtx);
567
+ // SEP-1865 ui:// resources for Generative UI primitives
568
+ // Uses McpServer.resource() high-level API with a ResourceTemplate so that
569
+ // resources/list (via listCallback) and resources/read both work.
570
+ // URI pattern: ui://vp/v1/{primitive} — query params read from the URL object.
571
+ const uiResourceTemplate = new ResourceTemplate("ui://vp/v1/{primitive}", {
572
+ list: async () => ({ resources: listUiResources() }),
573
+ });
574
+ server.resource("vp-ui", uiResourceTemplate, {
575
+ description: "SEP-1865 VantagePeers Generative UI primitives (HTML inline, Shadow DOM scoped)",
576
+ }, async (uri) => {
577
+ const fetchConvex = async (functionName, args) => {
578
+ // biome-ignore lint/suspicious/noExplicitAny: Convex string API
579
+ return convex.query(functionName, args);
580
+ };
581
+ const resource = await readUiResource(uri.toString(), fetchConvex);
582
+ return {
583
+ contents: [
584
+ {
585
+ uri: resource.uri,
586
+ mimeType: resource.mimeType,
587
+ text: resource.text,
588
+ },
589
+ ],
590
+ };
591
+ });
566
592
  const transport = new WebStandardStreamableHTTPServerTransport();
567
593
  await server.connect(transport);
568
594
  return transport.handleRequest(c.req.raw);
@@ -96,6 +96,7 @@ export declare const fieldsSchema: z.ZodEnum<{
96
96
  lite: "lite";
97
97
  full: "full";
98
98
  }>;
99
+ export declare const updatedSinceSchema: z.ZodNumber;
99
100
  export interface ParsedConvexError {
100
101
  code: string;
101
102
  message: string;
package/dist/src/tools.js CHANGED
@@ -10,6 +10,36 @@
10
10
  import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
11
11
  import { z } from "zod";
12
12
  import { checkFromAllowed, checkNamespaceRead, checkNamespaceWrite, isMasterScope, } from "./auth.js";
13
+ import { wrapToolResult } from "./ui-resources/stream-marker.js";
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+ // VP_EMIT_UI_MARKERS gate
16
+ //
17
+ // When VP_EMIT_UI_MARKERS=1 the 6 list/get tools that have a matching
18
+ // ui:// primitive append a __VP_TOOL_RESULT__<json>__END__ marker after the
19
+ // existing JSON payload. The Gen UI iframe bridge detects this marker and
20
+ // renders the structured primitive inline. Default is OFF so prod behaviour
21
+ // is unchanged.
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ const UI_MARKERS_ENABLED = process.env.VP_EMIT_UI_MARKERS === "1" ||
24
+ process.env.VP_EMIT_UI_MARKERS === "true";
25
+ /**
26
+ * Append a stream marker to a text response when UI markers are enabled.
27
+ * `buildPayload` is called only when the flag is ON to avoid any overhead.
28
+ */
29
+ function appendMarkerIfEnabled(text, buildPayload) {
30
+ if (!UI_MARKERS_ENABLED)
31
+ return text;
32
+ try {
33
+ const payload = buildPayload();
34
+ if (payload === null)
35
+ return text;
36
+ return `${text}\n${wrapToolResult(payload)}`;
37
+ }
38
+ catch {
39
+ // Never break the primary response — marker emission is best-effort.
40
+ return text;
41
+ }
42
+ }
13
43
  // ─────────────────────────────────────────────────────────────────────────────
14
44
  // Pre-flight content size guard
15
45
  //
@@ -162,6 +192,14 @@ export const fieldsSchema = z
162
192
  .describe('Field projection — "lite" returns compact fields only ' +
163
193
  '(typical 5-10× smaller payload for large list scans), ' +
164
194
  '"full" (default) returns the full document.');
195
+ // v2.3.3 — Unix timestamp ms filter for "updated since".
196
+ // Pass `Date.now() - 24*60*60*1000` for "last 24h" pattern.
197
+ export const updatedSinceSchema = z
198
+ .number()
199
+ .int()
200
+ .positive()
201
+ .describe("Unix timestamp (ms) — return only rows whose updatedAt >= this value. " +
202
+ "Typical usage: Date.now() - 24*60*60*1000 for last-24h window.");
165
203
  const mandateStatusSchema = z
166
204
  .enum(["requested", "accepted", "in_progress", "delivered", "settled"])
167
205
  .describe("Mandate lifecycle status");
@@ -738,13 +776,24 @@ export function registerTools(server, convex, oauthCtx) {
738
776
  type,
739
777
  limit: limit ?? 20,
740
778
  });
779
+ const rawList = Array.isArray(memories)
780
+ ? memories
781
+ : Array.isArray(memories?.page)
782
+ ? memories.page
783
+ : [];
784
+ const baseText = JSON.stringify(memories, null, 2);
785
+ const text = appendMarkerIfEnabled(baseText, () => ({
786
+ kind: "memory-quote",
787
+ items: rawList.map((m) => ({
788
+ _id: m._id,
789
+ namespace: m.namespace,
790
+ type: m.type,
791
+ content: m.content,
792
+ score: m.score,
793
+ })),
794
+ }));
741
795
  return {
742
- content: [
743
- {
744
- type: "text",
745
- text: JSON.stringify(memories, null, 2),
746
- },
747
- ],
796
+ content: [{ type: "text", text }],
748
797
  };
749
798
  }
750
799
  catch (error) {
@@ -1017,13 +1066,21 @@ export function registerTools(server, convex, oauthCtx) {
1017
1066
  from,
1018
1067
  limit: limit ?? 100,
1019
1068
  });
1069
+ const baseText = JSON.stringify(messages, null, 2);
1070
+ const text = appendMarkerIfEnabled(baseText, () => ({
1071
+ kind: "messages-feed",
1072
+ items: Array.isArray(messages)
1073
+ ? messages.map((m) => ({
1074
+ _id: m._id,
1075
+ from: m.from,
1076
+ channel: m.channel,
1077
+ content: m.content,
1078
+ createdAt: m.createdAt,
1079
+ }))
1080
+ : [],
1081
+ }));
1020
1082
  return {
1021
- content: [
1022
- {
1023
- type: "text",
1024
- text: JSON.stringify(messages, null, 2),
1025
- },
1026
- ],
1083
+ content: [{ type: "text", text }],
1027
1084
  };
1028
1085
  }
1029
1086
  catch (error) {
@@ -1141,28 +1198,42 @@ export function registerTools(server, convex, oauthCtx) {
1141
1198
  .min(1)
1142
1199
  .max(200)
1143
1200
  .optional()
1144
- .default(50)
1145
- .describe("Maximum number of tasks to return (default 50)"),
1201
+ .describe("Maximum number of tasks to return. Default 50 with fields=lite, auto-clamped to 30 when fields=full and no explicit limit (overflow protection)."),
1146
1202
  fields: fieldsSchema
1147
1203
  .optional()
1148
1204
  .describe('Field projection ("lite"|"full")'),
1149
- }, async ({ assignedTo, assignedToInstance, status, project, limit, fields, }) => {
1205
+ createdBy: assigneeSchema
1206
+ .optional()
1207
+ .describe("Filter by task creator (e.g. 'pi' to find Pi-dispatched tasks)"),
1208
+ updatedSince: updatedSinceSchema.optional(),
1209
+ }, async ({ assignedTo, assignedToInstance, status, project, limit, fields, createdBy, updatedSince, }) => {
1150
1210
  try {
1151
1211
  const tasks = await convex.query("tasks:list", {
1152
1212
  assignedTo,
1153
1213
  assignedToInstance,
1154
1214
  status,
1155
1215
  project,
1156
- limit: limit ?? 50,
1216
+ limit,
1157
1217
  fields,
1158
- });
1218
+ createdBy,
1219
+ updatedSince,
1220
+ });
1221
+ const baseText = JSON.stringify(tasks, null, 2);
1222
+ const text = appendMarkerIfEnabled(baseText, () => ({
1223
+ kind: "tasks-table",
1224
+ items: Array.isArray(tasks)
1225
+ ? tasks.map((t) => ({
1226
+ _id: t._id,
1227
+ title: t.title,
1228
+ status: t.status,
1229
+ priority: t.priority,
1230
+ assignedTo: t.assignedTo,
1231
+ _creationTime: t._creationTime,
1232
+ }))
1233
+ : [],
1234
+ }));
1159
1235
  return {
1160
- content: [
1161
- {
1162
- type: "text",
1163
- text: JSON.stringify(tasks, null, 2),
1164
- },
1165
- ],
1236
+ content: [{ type: "text", text }],
1166
1237
  };
1167
1238
  }
1168
1239
  catch (error) {
@@ -1470,18 +1541,23 @@ export function registerTools(server, convex, oauthCtx) {
1470
1541
  .min(1)
1471
1542
  .max(200)
1472
1543
  .optional()
1473
- .default(50)
1474
- .describe("Maximum number of tasks to return (default 50)"),
1544
+ .describe("Maximum number of tasks to return. Default 50 with fields=lite, auto-clamped to 30 when fields=full and no explicit limit."),
1475
1545
  fields: fieldsSchema
1476
1546
  .optional()
1477
1547
  .describe('Field projection ("lite"|"full")'),
1478
- }, async ({ missionId, status, limit, fields }) => {
1548
+ createdBy: assigneeSchema
1549
+ .optional()
1550
+ .describe("Filter by task creator"),
1551
+ updatedSince: updatedSinceSchema.optional(),
1552
+ }, async ({ missionId, status, limit, fields, createdBy, updatedSince }) => {
1479
1553
  try {
1480
1554
  const tasks = await convex.query("tasks:listByMission", {
1481
1555
  missionId: missionId,
1482
1556
  status,
1483
- limit: limit ?? 50,
1557
+ limit,
1484
1558
  fields,
1559
+ createdBy,
1560
+ updatedSince,
1485
1561
  });
1486
1562
  return {
1487
1563
  content: [
@@ -1565,27 +1641,38 @@ export function registerTools(server, convex, oauthCtx) {
1565
1641
  .min(1)
1566
1642
  .max(200)
1567
1643
  .optional()
1568
- .default(50)
1569
- .describe("Maximum number of missions to return (default 50)"),
1644
+ .describe("Maximum number of missions to return. Default 50 with fields=lite, auto-clamped to 30 when fields=full and no explicit limit."),
1570
1645
  fields: fieldsSchema
1571
1646
  .optional()
1572
1647
  .describe('Field projection ("lite"|"full")'),
1573
- }, async ({ project, pilot, status, limit, fields }) => {
1648
+ updatedSince: updatedSinceSchema.optional(),
1649
+ }, async ({ project, pilot, status, limit, fields, updatedSince }) => {
1574
1650
  try {
1575
1651
  const missions = await convex.query("missions:list", {
1576
1652
  project,
1577
1653
  pilot,
1578
1654
  status,
1579
- limit: limit ?? 50,
1655
+ limit,
1580
1656
  fields,
1581
- });
1657
+ updatedSince,
1658
+ });
1659
+ const baseText = JSON.stringify(missions, null, 2);
1660
+ const text = appendMarkerIfEnabled(baseText, () => ({
1661
+ kind: "mission-timeline",
1662
+ items: Array.isArray(missions)
1663
+ ? missions.map((m) => ({
1664
+ _id: m._id,
1665
+ name: m.name,
1666
+ project: m.project,
1667
+ status: m.status,
1668
+ pilot: m.pilot,
1669
+ priority: m.priority,
1670
+ progress: m.progress,
1671
+ }))
1672
+ : [],
1673
+ }));
1582
1674
  return {
1583
- content: [
1584
- {
1585
- type: "text",
1586
- text: JSON.stringify(missions, null, 2),
1587
- },
1588
- ],
1675
+ content: [{ type: "text", text }],
1589
1676
  };
1590
1677
  }
1591
1678
  catch (error) {
@@ -1740,13 +1827,24 @@ export function registerTools(server, convex, oauthCtx) {
1740
1827
  date,
1741
1828
  orchestrator,
1742
1829
  });
1743
- return {
1744
- content: [
1745
- {
1746
- type: "text",
1747
- text: JSON.stringify(entry, null, 2),
1830
+ const baseText = JSON.stringify(entry, null, 2);
1831
+ const text = appendMarkerIfEnabled(baseText, () => {
1832
+ if (!entry)
1833
+ return null;
1834
+ return {
1835
+ kind: "diary-entry",
1836
+ item: {
1837
+ _id: entry._id,
1838
+ date: entry.date,
1839
+ orchestrator: entry.orchestrator,
1840
+ content: entry.content,
1841
+ highlights: entry.highlights,
1842
+ blockers: entry.blockers,
1748
1843
  },
1749
- ],
1844
+ };
1845
+ });
1846
+ return {
1847
+ content: [{ type: "text", text }],
1750
1848
  };
1751
1849
  }
1752
1850
  catch (error) {
@@ -1899,25 +1997,40 @@ export function registerTools(server, convex, oauthCtx) {
1899
1997
  .min(1)
1900
1998
  .max(100)
1901
1999
  .optional()
1902
- .default(20)
1903
- .describe("Maximum notes to return (default 20)"),
2000
+ .describe("Maximum notes to return. Default 20 with fields=lite, auto-clamped to 15 when fields=full and no explicit limit."),
1904
2001
  fields: fieldsSchema
1905
2002
  .optional()
1906
2003
  .describe('Field projection ("lite"|"full")'),
1907
- }, async ({ topic, limit, fields }) => {
2004
+ updatedSince: updatedSinceSchema.optional(),
2005
+ }, async ({ topic, limit, fields, updatedSince }) => {
1908
2006
  try {
1909
2007
  const notes = await convex.query("briefingNotes:list", {
1910
2008
  topic,
1911
- limit: limit ?? 20,
2009
+ limit,
1912
2010
  fields,
2011
+ updatedSince,
2012
+ });
2013
+ const baseText = JSON.stringify(notes, null, 2);
2014
+ const text = appendMarkerIfEnabled(baseText, () => {
2015
+ const items = Array.isArray(notes) ? notes : [];
2016
+ if (items.length === 0)
2017
+ return null;
2018
+ // Emit the first note as a briefing-note item for the primitive renderer.
2019
+ const first = items[0];
2020
+ return {
2021
+ kind: "briefing-note",
2022
+ item: {
2023
+ _id: first._id,
2024
+ topic: first.topic,
2025
+ title: first.title,
2026
+ participants: first.participants,
2027
+ content: first.content,
2028
+ createdBy: first.createdBy,
2029
+ },
2030
+ };
1913
2031
  });
1914
2032
  return {
1915
- content: [
1916
- {
1917
- type: "text",
1918
- text: JSON.stringify(notes, null, 2),
1919
- },
1920
- ],
2033
+ content: [{ type: "text", text }],
1921
2034
  };
1922
2035
  }
1923
2036
  catch (error) {
@@ -0,0 +1,36 @@
1
+ /**
2
+ * SEP-1865 ui:// resources for VantagePeers Generative UI.
3
+ *
4
+ * URI pattern : ui://vp/v1/<primitive>?<query>
5
+ * Examples :
6
+ * ui://vp/v1/tasks-table?assignedTo=pi&status=review&fields=lite&limit=10
7
+ * ui://vp/v1/messages-feed?recipient=sigma&limit=20
8
+ *
9
+ * M1 scope : 1 primitive (tasks-table) — proves the pipeline.
10
+ * M2 scope : ≥6 primitives (tasks/messages/diary/missions/briefingNotes/memories).
11
+ *
12
+ * Pattern Hybrid 60% static lit-ui + 11% Gen UI + 27% Hybrid (cf vp-gui-views-research-2026-05-28.md).
13
+ * Returns HTML inline with embedded JS + CSS Shadow DOM scoped.
14
+ *
15
+ * Reference instance Theta : theta-vantage-crm-gui-iframe-embed-v1 (blissful-gopher-531).
16
+ * Mission Sigma : sigma-vantage-peers-mcp-gui-iframe-embed-v1 (k5730xct6rvrwkvxhy5t5js12d87jwfw).
17
+ */
18
+ export type UiResourceParsed = {
19
+ primitive: string;
20
+ query: URLSearchParams;
21
+ };
22
+ export declare function parseUiUri(uri: string): UiResourceParsed | null;
23
+ export declare const PRIMITIVES: readonly ["tasks-table", "messages-feed", "diary-entry", "mission-timeline", "briefing-note", "memory-quote"];
24
+ export type Primitive = (typeof PRIMITIVES)[number];
25
+ export declare const PRIMITIVE_DESCRIPTIONS: Record<Primitive, string>;
26
+ export declare function listUiResources(): Array<{
27
+ uri: string;
28
+ name: string;
29
+ description: string;
30
+ mimeType: string;
31
+ }>;
32
+ export declare function readUiResource(uri: string, fetchConvex: (functionName: string, args: Record<string, unknown>) => Promise<unknown>): Promise<{
33
+ uri: string;
34
+ mimeType: string;
35
+ text: string;
36
+ }>;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * SEP-1865 ui:// resources for VantagePeers Generative UI.
3
+ *
4
+ * URI pattern : ui://vp/v1/<primitive>?<query>
5
+ * Examples :
6
+ * ui://vp/v1/tasks-table?assignedTo=pi&status=review&fields=lite&limit=10
7
+ * ui://vp/v1/messages-feed?recipient=sigma&limit=20
8
+ *
9
+ * M1 scope : 1 primitive (tasks-table) — proves the pipeline.
10
+ * M2 scope : ≥6 primitives (tasks/messages/diary/missions/briefingNotes/memories).
11
+ *
12
+ * Pattern Hybrid 60% static lit-ui + 11% Gen UI + 27% Hybrid (cf vp-gui-views-research-2026-05-28.md).
13
+ * Returns HTML inline with embedded JS + CSS Shadow DOM scoped.
14
+ *
15
+ * Reference instance Theta : theta-vantage-crm-gui-iframe-embed-v1 (blissful-gopher-531).
16
+ * Mission Sigma : sigma-vantage-peers-mcp-gui-iframe-embed-v1 (k5730xct6rvrwkvxhy5t5js12d87jwfw).
17
+ */
18
+ import { renderBriefingNote } from "./primitives/briefing-note.js";
19
+ import { renderDiaryEntry } from "./primitives/diary-entry.js";
20
+ import { renderMemoryQuote } from "./primitives/memory-quote.js";
21
+ import { renderMessagesFeed } from "./primitives/messages-feed.js";
22
+ import { renderMissionTimeline } from "./primitives/mission-timeline.js";
23
+ import { renderTasksTable } from "./primitives/tasks-table.js";
24
+ // URI parser : ui://vp/v1/<primitive>?<query>
25
+ const UI_URI_RE = /^ui:\/\/vp\/v1\/([a-z][a-z0-9-]*)(?:\?(.*))?$/;
26
+ export function parseUiUri(uri) {
27
+ const match = UI_URI_RE.exec(uri);
28
+ if (!match)
29
+ return null;
30
+ const primitive = match[1];
31
+ const queryString = match[2] ?? "";
32
+ return {
33
+ primitive,
34
+ query: new URLSearchParams(queryString),
35
+ };
36
+ }
37
+ // Primitive registry — M1 ships 1 (tasks-table). M2 adds 5 more.
38
+ export const PRIMITIVES = [
39
+ "tasks-table",
40
+ "messages-feed",
41
+ "diary-entry",
42
+ "mission-timeline",
43
+ "briefing-note",
44
+ "memory-quote",
45
+ ];
46
+ export const PRIMITIVE_DESCRIPTIONS = {
47
+ "tasks-table": "Render a compact table of tasks. Query params: assignedTo, status, fields=lite|full, limit. Mirrors list_tasks tool semantics.",
48
+ "messages-feed": "Render a chronological feed of VantagePeers messages. Query params: from, channel, limit, lang.",
49
+ "diary-entry": "Render a single diary entry or list of recent entries. Query params: date (YYYY-MM-DD), orchestrator, limit, lang.",
50
+ "mission-timeline": "Render a missions timeline. Query params: pilot, project, status, limit, lang.",
51
+ "briefing-note": "Render briefing note details. Query params: noteId or (topic + limit), lang.",
52
+ "memory-quote": "Render memory quotes from a namespace. Query params: namespace, type, limit, lang.",
53
+ };
54
+ // Resource list — returned by resources/list MCP handler
55
+ export function listUiResources() {
56
+ return PRIMITIVES.map((p) => ({
57
+ uri: `ui://vp/v1/${p}`,
58
+ name: p,
59
+ description: PRIMITIVE_DESCRIPTIONS[p],
60
+ mimeType: "text/html",
61
+ }));
62
+ }
63
+ // Resource read — dispatched by primitive name. Returns HTML inline.
64
+ export async function readUiResource(uri, fetchConvex) {
65
+ const parsed = parseUiUri(uri);
66
+ if (!parsed) {
67
+ throw new Error(`[VP UI Resources] Invalid ui:// URI: ${uri}`);
68
+ }
69
+ if (!PRIMITIVES.includes(parsed.primitive)) {
70
+ throw new Error(`[VP UI Resources] Unknown primitive: ${parsed.primitive}. Available: ${PRIMITIVES.join(", ")}`);
71
+ }
72
+ let html;
73
+ switch (parsed.primitive) {
74
+ case "tasks-table":
75
+ html = await renderTasksTable(parsed.query, fetchConvex);
76
+ break;
77
+ case "messages-feed":
78
+ html = await renderMessagesFeed(parsed.query, fetchConvex);
79
+ break;
80
+ case "diary-entry":
81
+ html = await renderDiaryEntry(parsed.query, fetchConvex);
82
+ break;
83
+ case "mission-timeline":
84
+ html = await renderMissionTimeline(parsed.query, fetchConvex);
85
+ break;
86
+ case "briefing-note":
87
+ html = await renderBriefingNote(parsed.query, fetchConvex);
88
+ break;
89
+ case "memory-quote":
90
+ html = await renderMemoryQuote(parsed.query, fetchConvex);
91
+ break;
92
+ default:
93
+ throw new Error(`[VP UI Resources] Unimplemented primitive: ${parsed.primitive}`);
94
+ }
95
+ return {
96
+ uri,
97
+ mimeType: "text/html",
98
+ text: html,
99
+ };
100
+ }