Files
2026-03-13 15:00:22 +00:00

5.3 KiB

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 <jwt>, x-xtrem-endpoint: <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