|
| 1 | +/** |
| 2 | + * Manual-testing fixture for the diary map MCP app (issue #1194). |
| 3 | + * |
| 4 | + * Mints a throwaway agent against the RUNNING local e2e stack, seeds a handful |
| 5 | + * of namespaced-tag entries, builds a `entries_map_open` payload with a couple |
| 6 | + * of pre-interpreted zones, and prints a ready-to-open mcp-host URL. The agent + |
| 7 | + * diary persist in the stack (teardown only closes the DB pool), so the printed |
| 8 | + * URL stays valid for the life of the stack. |
| 9 | + * |
| 10 | + * Prereq: the e2e stack must be up (it runs the dockerized mcp-host on :8082): |
| 11 | + * COMPOSE_DISABLE_ENV_FILE=true docker compose -f docker-compose.e2e.yaml up -d --build |
| 12 | + * |
| 13 | + * Run (from repo root): |
| 14 | + * pnpm exec nx run @moltnet/mcp-host-e2e:diary-map-fixture |
| 15 | + * or directly: |
| 16 | + * pnpm --filter @moltnet/mcp-host-e2e exec tsx manual/diary-map-fixture.ts |
| 17 | + * |
| 18 | + * Then open the printed URL in a browser. See apps/mcp-host/README.md. |
| 19 | + */ |
| 20 | +import { writeFile } from 'node:fs/promises'; |
| 21 | + |
| 22 | +import { createMcpTestHarness } from '@moltnet/mcp-test-harness'; |
| 23 | + |
| 24 | +const HOST_BASE = process.env.MCP_HOST_URL ?? 'http://localhost:8082'; |
| 25 | + |
| 26 | +interface SeedSpec { |
| 27 | + content: string; |
| 28 | + title: string; |
| 29 | + tags: string[]; |
| 30 | + entryType: 'semantic' | 'episodic' | 'procedural' | 'reflection'; |
| 31 | +} |
| 32 | + |
| 33 | +const SEED: SeedSpec[] = [ |
| 34 | + { |
| 35 | + title: 'Chose Drizzle migrations over raw SQL', |
| 36 | + content: |
| 37 | + 'Decided to manage the schema with Drizzle migrations rather than hand-written SQL, for type-safe diffs and a reviewable migration log.', |
| 38 | + tags: ['scope:infra', 'topic:database', 'decision'], |
| 39 | + entryType: 'semantic', |
| 40 | + }, |
| 41 | + { |
| 42 | + title: 'Adopted Ory Keto for authorization', |
| 43 | + content: |
| 44 | + 'Authorization is modeled in Ory Keto (relation tuples) instead of in-app role checks, so policy lives outside the services.', |
| 45 | + tags: ['scope:infra', 'scope:auth', 'decision'], |
| 46 | + entryType: 'semantic', |
| 47 | + }, |
| 48 | + { |
| 49 | + title: 'Postgres + pgvector for hybrid search', |
| 50 | + content: |
| 51 | + 'Picked Postgres with pgvector so semantic + full-text search share one store; avoids a separate vector DB to operate.', |
| 52 | + tags: ['scope:infra', 'topic:database', 'topic:search'], |
| 53 | + entryType: 'semantic', |
| 54 | + }, |
| 55 | + { |
| 56 | + title: 'Reflected on agent autonomy boundaries', |
| 57 | + content: |
| 58 | + 'Thinking through how much an agent should decide alone vs. ask the human — leaning toward explicit, auditable accountability over silent autonomy.', |
| 59 | + tags: ['topic:autonomy', 'reflection'], |
| 60 | + entryType: 'reflection', |
| 61 | + }, |
| 62 | + { |
| 63 | + title: 'Identity is cryptographic, not account-based', |
| 64 | + content: |
| 65 | + 'An agent owns an Ed25519 key; identity and signatures are the source of truth, not a human-owned account row.', |
| 66 | + tags: ['topic:autonomy', 'topic:identity', 'decision'], |
| 67 | + entryType: 'semantic', |
| 68 | + }, |
| 69 | + { |
| 70 | + title: 'Rejected a server-side LLM for exploration', |
| 71 | + content: |
| 72 | + 'Considered running interpretation on the server; rejected it to keep the server deterministic and retrieval-only. The client agent interprets.', |
| 73 | + tags: ['topic:research', 'scope:mcp-apps', 'decision'], |
| 74 | + entryType: 'semantic', |
| 75 | + }, |
| 76 | + { |
| 77 | + title: 'MCP app foundation moved to ext-apps', |
| 78 | + content: |
| 79 | + 'UI apps now build on @modelcontextprotocol/ext-apps in dedicated Vite libs, with a host fixture + browser e2e.', |
| 80 | + tags: ['scope:mcp-apps', 'topic:research'], |
| 81 | + entryType: 'procedural', |
| 82 | + }, |
| 83 | +]; |
| 84 | + |
| 85 | +async function main() { |
| 86 | + const harness = await createMcpTestHarness(); |
| 87 | + const { agent, privateDiaryId, restApiUrl, personalTeamId } = harness; |
| 88 | + |
| 89 | + async function seed(spec: SeedSpec): Promise<string> { |
| 90 | + const response = await fetch( |
| 91 | + `${restApiUrl}/diaries/${privateDiaryId}/entries`, |
| 92 | + { |
| 93 | + method: 'POST', |
| 94 | + headers: { |
| 95 | + 'content-type': 'application/json', |
| 96 | + authorization: `Bearer ${agent.accessToken}`, |
| 97 | + 'x-moltnet-team-id': personalTeamId, |
| 98 | + }, |
| 99 | + body: JSON.stringify(spec), |
| 100 | + }, |
| 101 | + ); |
| 102 | + if (!response.ok) { |
| 103 | + throw new Error( |
| 104 | + `seed failed: ${response.status} ${await response.text()}`, |
| 105 | + ); |
| 106 | + } |
| 107 | + return ((await response.json()) as { id: string }).id; |
| 108 | + } |
| 109 | + |
| 110 | + const ids = await Promise.all(SEED.map(seed)); |
| 111 | + const idOf = (titlePart: string) => |
| 112 | + ids[SEED.findIndex((s) => s.title.includes(titlePart))]; |
| 113 | + |
| 114 | + // A pre-interpreted map with three zones, mirroring what the agent would push. |
| 115 | + const map = { |
| 116 | + diaryName: 'Manual fixture diary', |
| 117 | + totalEntries: SEED.length, |
| 118 | + sampledEntries: SEED.length, |
| 119 | + overview: |
| 120 | + 'A small diary with three zones: infrastructure decisions, autonomy & identity, and research/MCP-app work.', |
| 121 | + zones: [ |
| 122 | + { |
| 123 | + id: 'infra', |
| 124 | + label: 'Infrastructure decisions', |
| 125 | + why: 'Database, search, and authorization choices that shape the stack.', |
| 126 | + territory: 'scope:infra', |
| 127 | + entryIds: [idOf('Drizzle'), idOf('Keto'), idOf('pgvector')], |
| 128 | + provenance: { |
| 129 | + basis: 'tag:scope:infra', |
| 130 | + searches: [{ tags: ['scope:infra'] }], |
| 131 | + }, |
| 132 | + }, |
| 133 | + { |
| 134 | + id: 'autonomy', |
| 135 | + label: 'Autonomy & identity', |
| 136 | + why: 'Reflections and decisions about agent autonomy and cryptographic identity.', |
| 137 | + territory: 'topic:autonomy', |
| 138 | + entryIds: [idOf('autonomy boundaries'), idOf('cryptographic')], |
| 139 | + provenance: { |
| 140 | + basis: 'tag:topic:autonomy', |
| 141 | + searches: [{ tags: ['topic:autonomy'] }], |
| 142 | + }, |
| 143 | + }, |
| 144 | + { |
| 145 | + id: 'research', |
| 146 | + label: 'Research & MCP apps', |
| 147 | + why: 'Exploration of the MCP app foundation and design research.', |
| 148 | + territory: 'scope:mcp-apps', |
| 149 | + entryIds: [idOf('server-side LLM'), idOf('ext-apps')], |
| 150 | + provenance: { |
| 151 | + basis: 'tag:scope:mcp-apps', |
| 152 | + searches: [{ tags: ['scope:mcp-apps'] }], |
| 153 | + }, |
| 154 | + }, |
| 155 | + ], |
| 156 | + }; |
| 157 | + |
| 158 | + const url = new URL('/', HOST_BASE); |
| 159 | + url.searchParams.set('tool', 'entries_map_open'); |
| 160 | + url.searchParams.set('autorun', '1'); |
| 161 | + url.searchParams.set('server', `${harness.mcpBaseUrl}/mcp`); |
| 162 | + url.searchParams.set('clientId', agent.clientId); |
| 163 | + // Throwaway fixture credentials for a local ephemeral stack ONLY. Putting a |
| 164 | + // secret in URL params leaks it to browser history / logs / Referer headers — |
| 165 | + // never do this with real agent credentials. |
| 166 | + url.searchParams.set('clientSecret', agent.clientSecret); |
| 167 | + url.searchParams.set( |
| 168 | + 'args', |
| 169 | + JSON.stringify({ diary_id: privateDiaryId, map }), |
| 170 | + ); |
| 171 | + |
| 172 | + await harness.teardown(); // closes the DB pool; the agent + diary persist. |
| 173 | + |
| 174 | + // Write the URL to a file too — copying a 1.7k single-line URL out of a |
| 175 | + // terminal/chat is error-prone (spaces/line-breaks corrupt the JSON args). |
| 176 | + const outFile = new URL('./diary-map-url.txt', import.meta.url); |
| 177 | + await writeFile(outFile, url.toString() + '\n', 'utf8'); |
| 178 | + |
| 179 | + // eslint-disable-next-line no-console |
| 180 | + console.log('\n=== Diary map manual fixture ready ==='); |
| 181 | + // eslint-disable-next-line no-console |
| 182 | + console.log(`agent fingerprint : ${agent.keyPair.fingerprint}`); |
| 183 | + // eslint-disable-next-line no-console |
| 184 | + console.log(`diary id : ${privateDiaryId}`); |
| 185 | + // eslint-disable-next-line no-console |
| 186 | + console.log(`seeded entries : ${ids.length}`); |
| 187 | + // eslint-disable-next-line no-console |
| 188 | + console.log( |
| 189 | + '\nURL written to apps/mcp-host-e2e/manual/diary-map-url.txt' + |
| 190 | + ' (cat it / scp it to avoid copy corruption).', |
| 191 | + ); |
| 192 | + // eslint-disable-next-line no-console |
| 193 | + console.log( |
| 194 | + '\nOver SSH, forward the ports first:\n' + |
| 195 | + ' ssh -L 8082:localhost:8082 -L 8083:localhost:8083 -L 8001:localhost:8001 <host>\n' + |
| 196 | + 'then open (the dockerized mcp-host on :8082):\n', |
| 197 | + ); |
| 198 | + // eslint-disable-next-line no-console |
| 199 | + console.log(url.toString()); |
| 200 | + // eslint-disable-next-line no-console |
| 201 | + console.log(''); |
| 202 | +} |
| 203 | + |
| 204 | +main().catch((error: unknown) => { |
| 205 | + // eslint-disable-next-line no-console |
| 206 | + console.error( |
| 207 | + error instanceof Error ? error.message : String(error), |
| 208 | + '\n\nIs the e2e stack up? Run:\n COMPOSE_DISABLE_ENV_FILE=true docker compose -f docker-compose.e2e.yaml up -d --build', |
| 209 | + ); |
| 210 | + process.exit(1); |
| 211 | +}); |
0 commit comments