# Learnings ## Session Start - Greenfield project — no existing code, only opencode.json + planner_instructions.md - No git repo initialized yet - No git worktrees — per planner instructions - Runtime: Bun (built-in TypeScript, fetch, test runner) - Dependencies: @modelcontextprotocol/sdk, zod, jose - Demo endpoint: https://api-devna.dev-sagex3.com/demo/service/X3CLOUDV2_SEED/api (unauthenticated, max 20 results) ## Task 1: Project Initialization ### MCP SDK Package - npm package is `@modelcontextprotocol/sdk` (NOT `@modelcontextprotocol/server`) - Import path uses subpath export: `import { McpServer, StdioServerTransport } from '@modelcontextprotocol/sdk/server'` - Zod import: `import * as z from 'zod/v4'` (v4 subpath) - Installed versions: `@modelcontextprotocol/sdk@1.27.1`, `jose@6.2.1`, `zod@4.3.6` ### Bun Runtime - `bun` not installed on this system; using `npx bun` as workaround - `bun init -y` creates index.ts, tsconfig.json, README.md, .gitignore - bun init tsconfig defaults to bundler mode — replaced with NodeNext for MCP compatibility ### Config Module Pattern - `import.meta.main` works in Bun to detect direct execution - `process.env` works in Bun (Node compat) - Auto-detect mode pattern: check presence of clientId + secret + user → "authenticated", else "sandbox" - JSON.stringify omits `undefined` properties — clean output for debugging ## Task 3: Auth Module ### jose Library (v6.2.1) - `new SignJWT(payload).setProtectedHeader({ alg: 'HS256' }).sign(secret)` — clean chaining API - Secret must be `Uint8Array` via `new TextEncoder().encode(secretString)` - Can pass all JWT claims directly in payload constructor — no need to use `.setIssuedAt()` etc. when custom values needed - Do NOT use `.setIssuedAt()` for X3 — need custom iat with -30s offset ### X3 JWT Claims - `iss` = clientId, `sub` = user, `aud` = "" (empty string) - `iat` = `Math.floor(Date.now() / 1000) - 30` — 30-second clock skew offset is CRITICAL - `exp` = `iat + tokenLifetime` - Algorithm: HS256 only ### Auth Headers Pattern - Sandbox: only `Content-Type: application/json` (no auth at all) - Authenticated: `Authorization: Bearer `, `x-xtrem-endpoint: `, `Content-Type: application/json` - JWT regenerated on each call (short-lived tokens, no caching) ## Task 5: MCP Prompts Module ### MCP SDK Prompt API (v2) - Use `server.registerPrompt()` (not deprecated `server.prompt()`) - Signature: `registerPrompt(name, { title?, description?, argsSchema? }, callback)` - argsSchema is a raw Zod shape object (NOT wrapped in `z.object()`) — the SDK wraps it internally - Callback: `(args, extra) => GetPromptResult` for prompts with args, `(extra) => GetPromptResult` for no-args - For no-args prompts, omit argsSchema from config — callback receives only `extra` param - Return type: `{ messages: [{ role: 'user', content: { type: 'text', text: string } }] }` - Import: `McpServer` from `@modelcontextprotocol/sdk/server/mcp.js` ## Task 4: GraphQL Client ### Dynamic Import for Parallel Task Dependencies - When module A depends on module B being created by a parallel task, use variable-path dynamic import - `const path = '../auth/index.js'; await import(path)` — TypeScript types it as `any`, no static resolution error - Allows compilation even when the target module doesn't exist yet ### Demo Endpoint Status (2026-03-13) - Demo endpoint `https://api-devna.dev-sagex3.com/demo/service/X3CLOUDV2_SEED/api` currently returns 401 - Response body: "Votre mot de passe a expiré. Vous devez le modifier" (French: password expired) - This is a server-side issue, not a client bug — our error handling catches it correctly ### Mutation Guard Pattern - Regex `/^mutation\b/i` on `query.trim()` catches: `mutation {`, `MUTATION {`, ` mutation {` - Word boundary `\b` prevents false positives on field names containing "mutation" ### TLS Override Pattern - Save `process.env.NODE_TLS_REJECT_UNAUTHORIZED` before, restore in `finally` block - If was undefined before, `delete` the env var rather than setting to undefined ## Task 3b: MCP Resources Module ### MCP SDK Resource API (v2) - Use `server.registerResource()` (not deprecated `server.resource()`) - Signature: `registerResource(name, uri, config, readCallback)` - config is `ResourceMetadata` = `{ description?, mimeType?, title?, annotations? }` - readCallback: `(uri: URL, extra) => { contents: [{ uri: string, text: string, mimeType?: string }] }` - Import: `McpServer` from `@modelcontextprotocol/sdk/server/mcp.js` - For static resources, pass URI as plain string (not ResourceTemplate) ## F1: Plan Compliance Audit Results ### Verification Evidence - **Typecheck**: `npx bun run typecheck` → clean (0 errors) - **Tests**: `npx bun test` → 51 pass, 0 fail, 167 expect() calls, 5 test files - **Mutation guard**: Two-layer defense — tool-level in `execute-graphql.ts` + client-level in `client.ts` - **All 22 src files inspected**: Simple structure, one file per tool, no over-abstraction - **MCP server protocol tested**: mcp-server.test.ts covers initialize, tools/list (5), resources/list (5), prompts/list (4), error handling ### Key Architecture Patterns - Tools use `server.registerTool()` or `server.tool()` with Zod v4 schemas - Resources registered in a loop from knowledge/ directory constants - Config auto-detects sandbox vs authenticated from env var presence - JWT uses jose SignJWT with 30-second clock skew offset per X3 docs