vantage-peers-mcp 2.3.5 → 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 +104 -0
- package/dist/server-http.js +27 -1
- package/dist/src/tools.js +129 -36
- package/dist/src/ui-resources/index.d.ts +36 -0
- package/dist/src/ui-resources/index.js +100 -0
- package/dist/src/ui-resources/primitives/briefing-note.d.ts +18 -0
- package/dist/src/ui-resources/primitives/briefing-note.js +126 -0
- package/dist/src/ui-resources/primitives/diary-entry.d.ts +18 -0
- package/dist/src/ui-resources/primitives/diary-entry.js +127 -0
- package/dist/src/ui-resources/primitives/memory-quote.d.ts +16 -0
- package/dist/src/ui-resources/primitives/memory-quote.js +109 -0
- package/dist/src/ui-resources/primitives/messages-feed.d.ts +16 -0
- package/dist/src/ui-resources/primitives/messages-feed.js +117 -0
- package/dist/src/ui-resources/primitives/mission-timeline.d.ts +17 -0
- package/dist/src/ui-resources/primitives/mission-timeline.js +157 -0
- package/dist/src/ui-resources/primitives/tasks-table.d.ts +17 -0
- package/dist/src/ui-resources/primitives/tasks-table.js +130 -0
- package/dist/src/ui-resources/schemas.d.ts +124 -0
- package/dist/src/ui-resources/schemas.js +88 -0
- package/dist/src/ui-resources/stream-marker.d.ts +33 -0
- package/dist/src/ui-resources/stream-marker.js +67 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,109 @@
|
|
|
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
|
+
|
|
3
107
|
## v2.3.5 — 2026-05-28
|
|
4
108
|
|
|
5
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.
|
package/dist/server-http.js
CHANGED
|
@@ -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);
|
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
|
//
|
|
@@ -746,13 +776,24 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
746
776
|
type,
|
|
747
777
|
limit: limit ?? 20,
|
|
748
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
|
+
}));
|
|
749
795
|
return {
|
|
750
|
-
content: [
|
|
751
|
-
{
|
|
752
|
-
type: "text",
|
|
753
|
-
text: JSON.stringify(memories, null, 2),
|
|
754
|
-
},
|
|
755
|
-
],
|
|
796
|
+
content: [{ type: "text", text }],
|
|
756
797
|
};
|
|
757
798
|
}
|
|
758
799
|
catch (error) {
|
|
@@ -1025,13 +1066,21 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
1025
1066
|
from,
|
|
1026
1067
|
limit: limit ?? 100,
|
|
1027
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
|
+
}));
|
|
1028
1082
|
return {
|
|
1029
|
-
content: [
|
|
1030
|
-
{
|
|
1031
|
-
type: "text",
|
|
1032
|
-
text: JSON.stringify(messages, null, 2),
|
|
1033
|
-
},
|
|
1034
|
-
],
|
|
1083
|
+
content: [{ type: "text", text }],
|
|
1035
1084
|
};
|
|
1036
1085
|
}
|
|
1037
1086
|
catch (error) {
|
|
@@ -1169,13 +1218,22 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
1169
1218
|
createdBy,
|
|
1170
1219
|
updatedSince,
|
|
1171
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
|
+
}));
|
|
1172
1235
|
return {
|
|
1173
|
-
content: [
|
|
1174
|
-
{
|
|
1175
|
-
type: "text",
|
|
1176
|
-
text: JSON.stringify(tasks, null, 2),
|
|
1177
|
-
},
|
|
1178
|
-
],
|
|
1236
|
+
content: [{ type: "text", text }],
|
|
1179
1237
|
};
|
|
1180
1238
|
}
|
|
1181
1239
|
catch (error) {
|
|
@@ -1598,13 +1656,23 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
1598
1656
|
fields,
|
|
1599
1657
|
updatedSince,
|
|
1600
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
|
+
}));
|
|
1601
1674
|
return {
|
|
1602
|
-
content: [
|
|
1603
|
-
{
|
|
1604
|
-
type: "text",
|
|
1605
|
-
text: JSON.stringify(missions, null, 2),
|
|
1606
|
-
},
|
|
1607
|
-
],
|
|
1675
|
+
content: [{ type: "text", text }],
|
|
1608
1676
|
};
|
|
1609
1677
|
}
|
|
1610
1678
|
catch (error) {
|
|
@@ -1759,13 +1827,24 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
1759
1827
|
date,
|
|
1760
1828
|
orchestrator,
|
|
1761
1829
|
});
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
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,
|
|
1767
1843
|
},
|
|
1768
|
-
|
|
1844
|
+
};
|
|
1845
|
+
});
|
|
1846
|
+
return {
|
|
1847
|
+
content: [{ type: "text", text }],
|
|
1769
1848
|
};
|
|
1770
1849
|
}
|
|
1771
1850
|
catch (error) {
|
|
@@ -1931,13 +2010,27 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
1931
2010
|
fields,
|
|
1932
2011
|
updatedSince,
|
|
1933
2012
|
});
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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,
|
|
1939
2029
|
},
|
|
1940
|
-
|
|
2030
|
+
};
|
|
2031
|
+
});
|
|
2032
|
+
return {
|
|
2033
|
+
content: [{ type: "text", text }],
|
|
1941
2034
|
};
|
|
1942
2035
|
}
|
|
1943
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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* briefing-note primitive — SEP-1865 ui:// resource.
|
|
3
|
+
* Renders a briefing note detail card or list of recent notes.
|
|
4
|
+
*
|
|
5
|
+
* Query params :
|
|
6
|
+
* noteId : Convex ID for a specific note (optional)
|
|
7
|
+
* topic : topic filter — used when noteId not provided (optional)
|
|
8
|
+
* limit : 1-100 (default 20) — used when noteId not provided
|
|
9
|
+
* lang : "en" (default) | "fr"
|
|
10
|
+
*
|
|
11
|
+
* Backend :
|
|
12
|
+
* briefingNotes:get — when noteId provided
|
|
13
|
+
* briefingNotes:list — when topic or no params
|
|
14
|
+
*
|
|
15
|
+
* Output : HTML card(s) wrapped in <div class="vp-briefing-note"> with embedded CSS.
|
|
16
|
+
* WCAG AA + bilingual FR+EN labels.
|
|
17
|
+
*/
|
|
18
|
+
export declare function renderBriefingNote(query: URLSearchParams, fetchConvex: (functionName: string, args: Record<string, unknown>) => Promise<unknown>): Promise<string>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* briefing-note primitive — SEP-1865 ui:// resource.
|
|
3
|
+
* Renders a briefing note detail card or list of recent notes.
|
|
4
|
+
*
|
|
5
|
+
* Query params :
|
|
6
|
+
* noteId : Convex ID for a specific note (optional)
|
|
7
|
+
* topic : topic filter — used when noteId not provided (optional)
|
|
8
|
+
* limit : 1-100 (default 20) — used when noteId not provided
|
|
9
|
+
* lang : "en" (default) | "fr"
|
|
10
|
+
*
|
|
11
|
+
* Backend :
|
|
12
|
+
* briefingNotes:get — when noteId provided
|
|
13
|
+
* briefingNotes:list — when topic or no params
|
|
14
|
+
*
|
|
15
|
+
* Output : HTML card(s) wrapped in <div class="vp-briefing-note"> with embedded CSS.
|
|
16
|
+
* WCAG AA + bilingual FR+EN labels.
|
|
17
|
+
*/
|
|
18
|
+
// Minimal escape — avoid XSS in injected content
|
|
19
|
+
function esc(s) {
|
|
20
|
+
return s.replace(/[&<>"']/g, (c) => {
|
|
21
|
+
switch (c) {
|
|
22
|
+
case "&":
|
|
23
|
+
return "&";
|
|
24
|
+
case "<":
|
|
25
|
+
return "<";
|
|
26
|
+
case ">":
|
|
27
|
+
return ">";
|
|
28
|
+
case '"':
|
|
29
|
+
return """;
|
|
30
|
+
case "'":
|
|
31
|
+
return "'";
|
|
32
|
+
default:
|
|
33
|
+
return c;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function renderNoteCard(note, lang) {
|
|
38
|
+
const participantsLabel = lang === "fr" ? "Participants" : "Participants";
|
|
39
|
+
const byLabel = lang === "fr" ? "Par" : "By";
|
|
40
|
+
const participantsHtml = note.participants && note.participants.length > 0
|
|
41
|
+
? `<div class="vp-briefing-participants">
|
|
42
|
+
<span class="vp-briefing-label">${esc(participantsLabel)} :</span>
|
|
43
|
+
${note.participants.map((p) => `<span class="vp-briefing-pill">${esc(p)}</span>`).join(" ")}
|
|
44
|
+
</div>`
|
|
45
|
+
: "";
|
|
46
|
+
const createdByHtml = note.createdBy
|
|
47
|
+
? `<div class="vp-briefing-meta">${esc(byLabel)}: ${esc(note.createdBy)}</div>`
|
|
48
|
+
: "";
|
|
49
|
+
const contentHtml = note.content
|
|
50
|
+
? `<div class="vp-briefing-content">${esc(note.content)}</div>`
|
|
51
|
+
: "";
|
|
52
|
+
return `<article class="vp-briefing-card" aria-label="${esc(note.title)}">
|
|
53
|
+
<header class="vp-briefing-header">
|
|
54
|
+
<span class="vp-briefing-topic">${esc(note.topic)}</span>
|
|
55
|
+
<h3 class="vp-briefing-title">${esc(note.title)}</h3>
|
|
56
|
+
</header>
|
|
57
|
+
${participantsHtml}
|
|
58
|
+
${contentHtml}
|
|
59
|
+
${createdByHtml}
|
|
60
|
+
</article>`;
|
|
61
|
+
}
|
|
62
|
+
export async function renderBriefingNote(query, fetchConvex) {
|
|
63
|
+
const noteId = query.get("noteId") ?? undefined;
|
|
64
|
+
const topic = query.get("topic") ?? undefined;
|
|
65
|
+
const limitRaw = query.get("limit");
|
|
66
|
+
const limitParsed = limitRaw !== null ? Number.parseInt(limitRaw, 10) : Number.NaN;
|
|
67
|
+
const limit = Number.isNaN(limitParsed)
|
|
68
|
+
? 20
|
|
69
|
+
: Math.min(100, Math.max(1, limitParsed));
|
|
70
|
+
const lang = (query.get("lang") ?? "en").toLowerCase();
|
|
71
|
+
const heading = lang === "fr"
|
|
72
|
+
? "Notes de briefing VantagePeers"
|
|
73
|
+
: "VantagePeers Briefing Notes";
|
|
74
|
+
const emptyLabel = lang === "fr"
|
|
75
|
+
? "Aucune note de briefing trouvée."
|
|
76
|
+
: "No briefing notes found.";
|
|
77
|
+
let notes = [];
|
|
78
|
+
try {
|
|
79
|
+
if (noteId) {
|
|
80
|
+
const result = (await fetchConvex("briefingNotes:get", {
|
|
81
|
+
noteId,
|
|
82
|
+
}));
|
|
83
|
+
notes = result ? [result] : [];
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const args = { limit };
|
|
87
|
+
if (topic)
|
|
88
|
+
args.topic = topic;
|
|
89
|
+
const result = (await fetchConvex("briefingNotes:list", args));
|
|
90
|
+
notes = Array.isArray(result) ? result : [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
95
|
+
return `<div class="vp-briefing-note-error" role="alert">${esc(msg)}</div>`;
|
|
96
|
+
}
|
|
97
|
+
const style = `<style>
|
|
98
|
+
.vp-briefing-note { font-family: system-ui, -apple-system, sans-serif; font-size: 13px; color: #1f2328; }
|
|
99
|
+
.vp-briefing-card { border: 1px solid #d0d7de; border-radius: 8px; padding: 16px; margin-bottom: 12px; background: #fff; }
|
|
100
|
+
.vp-briefing-header { margin-bottom: 10px; }
|
|
101
|
+
.vp-briefing-topic { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 500; background: #ddf4ff; color: #0969da; margin-bottom: 6px; }
|
|
102
|
+
.vp-briefing-title { font-size: 14px; font-weight: 600; margin: 0; }
|
|
103
|
+
.vp-briefing-participants { margin-top: 8px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
|
|
104
|
+
.vp-briefing-label { color: #656d76; }
|
|
105
|
+
.vp-briefing-pill { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; background: #f6f8fa; border: 1px solid #d0d7de; }
|
|
106
|
+
.vp-briefing-content { margin-top: 10px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
|
107
|
+
.vp-briefing-meta { margin-top: 8px; color: #656d76; font-size: 12px; }
|
|
108
|
+
.vp-briefing-count { color: #656d76; font-size: 12px; margin-top: 8px; }
|
|
109
|
+
.vp-briefing-empty { color: #656d76; padding: 12px 0; }
|
|
110
|
+
</style>`;
|
|
111
|
+
if (notes.length === 0) {
|
|
112
|
+
return `<div class="vp-briefing-note" role="region" aria-label="${esc(heading)}">
|
|
113
|
+
${style}
|
|
114
|
+
<p class="vp-briefing-empty">${esc(emptyLabel)}</p>
|
|
115
|
+
</div>`;
|
|
116
|
+
}
|
|
117
|
+
const cards = notes.map((n) => renderNoteCard(n, lang)).join("\n");
|
|
118
|
+
const countLabel = lang === "fr"
|
|
119
|
+
? `${notes.length} note${notes.length === 1 ? "" : "s"}`
|
|
120
|
+
: `${notes.length} note${notes.length === 1 ? "" : "s"}`;
|
|
121
|
+
return `<div class="vp-briefing-note" role="region" aria-label="${esc(heading)}">
|
|
122
|
+
${style}
|
|
123
|
+
${cards}
|
|
124
|
+
<div class="vp-briefing-count" aria-live="polite">${esc(countLabel)}</div>
|
|
125
|
+
</div>`;
|
|
126
|
+
}
|