# Project export: ContextMaster

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Agents forget everything between sessions. We give them persistent, shared memory, so 30 different engineers and 100 coding agents stay on the same page.
- Devpost: https://devpost.com/software/contextmaster
- GitHub: https://github.com/AbeBhatti/ContextMaster
- Video: https://www.youtube.com/embed/3ncNegtHz9k?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Abraham Bhatti (5 commits), Cursor (5 commits)

## Devpost submission (written by the team)

### Inspiration

As a developer, I've dealt with agent memory issues for months. My two biggest pain points were persistent memory and shareability. I tried storing all my agents' context in Obsidian markdown files, but that blew up my token costs and hurt my agent's performance, and it didn't solve sharing at all. As someone who competes in hackathons almost every week, I wanted a way to share context with my team so all our coding agents stay on the same page. So I built ContextMaster.

### What it does

Say you have a team of five builders, each using Claude, Cursor, and Gemini. All your agents can see the codebase, but the code doesn't tell the whole story. There's no record of why you made a decision, where you pivoted, or how the product reached its current state. ContextMaster connects all agents to a single workspace over MCP. Instead of copy-pasting context between agents and losing details, each agent selectively pushes and pulls from a shared "team brain", so every decision is tracked and every model stays up to date.

### How we built it

I built a custom MCP server with tools to push and pull context from specific workspaces (you can have as many as you want). When you save a chat session, gpt-4o-mini chunks it into typed categories: decisions, current state, conventions, findings, open questions, references, and context. Each chunk is embedded with text-embedding-3-small and stored in Redis. Every save is timestamped, so you can tell which decisions are current and which are old but still relevant, and superseded decisions get flagged automatically. For retrieval, I use RediSearch. Your query is embedded and two ranked lists come back: one ranks chunks by keyword match using BM25, the other by semantic similarity using K-nearest-neighbors with cosine distance over an HNSW vector index. I fuse the both lists, keyword + semantic, with Reciprocal Rank Fusion (RRF). The top chunks load into the agent's context, so it doesn't miss anything and doesn't have to load the entire project into its window. Vectors, full-text search, and storage all live in one Redis instance, no separate vector database.

### Challenges we ran into

The hardest challenge was keeping the agent faithful to the tool instead of falling back on its own memory, where it tends to miss details and hallucinate. Getting reliable retrieval took real experimentation before I landed on the BM25 + KNN/HNSW hybrid. To test this out, I turned off my agent memory and examined if it could pull catch specific details and important information from the context. The other challenge was minimizing overhead so developers don't feel like they're migrating to a new tool. You do 99% of the work right inside Claude or Cursor, make an account, connect your agents, and you're set.

### Accomplishments we're proud of

I'm proud of the retrieval performance — even with minimal LLM calls, agents save and retrieve details reliably. A big milestone was connecting multiple coding agents to a single shared workspace, so different agents stay in sync through one "team brain." I also architected the system for full team sharing: once it's deployed, users can share workspaces and connect their own agents across machines, I just couldn't demo that part solo on one computer.

### What we learned

I learned how to manage large amounts of context and why it matters, not just for coding agents. Context loss is a serious industry problem, and this project showed me that conservative, shared context isn't just about saving tokens; it directly improves an agent's performance.

### What's next

I'm planning to deploy this, it's a tool I'd genuinely use with my hackathon teams. I'd also love to bring it to local startups and see how they respond to this approach.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 150 recognized source files, 1570 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 166)

```
.dockerignore
.env.example
.gitignore
docker-compose.yml
package.json
packages/api/package.json
packages/api/src/data/gettingStartedKB.ts
packages/api/src/index.ts
packages/api/src/lib/access.ts
packages/api/src/lib/analytics.ts
packages/api/src/lib/apiKeyRepo.ts
packages/api/src/lib/bootstrap.ts
packages/api/src/lib/chunkRepo.ts
packages/api/src/lib/docRepo.ts
packages/api/src/lib/indexes.ts
packages/api/src/lib/inviteRepo.ts
packages/api/src/lib/kbRepo.ts
packages/api/src/lib/kbTemplates.ts
packages/api/src/lib/keys.ts
packages/api/src/lib/notificationRepo.ts
packages/api/src/lib/oauthRepo.ts
packages/api/src/lib/orgRepo.ts
packages/api/src/lib/provisioning.ts
packages/api/src/lib/redis.ts
packages/api/src/lib/search.ts
packages/api/src/lib/sessionRepo.ts
packages/api/src/lib/superCommitRepo.ts
packages/api/src/lib/types.ts
packages/api/src/lib/userRepo.ts
packages/api/src/lib/workspaceRepo.ts
packages/api/src/loadEnv.ts
packages/api/src/middleware/apiKeyAuth.ts
packages/api/src/middleware/auth.ts
packages/api/src/middleware/clerkAuth.ts
packages/api/src/routes/auth.ts
packages/api/src/routes/billing.ts
packages/api/src/routes/clerkWebhook.ts
packages/api/src/routes/mcp.ts
packages/api/src/routes/mcpProtocol.ts
packages/api/src/routes/mcpSSE.ts
packages/api/src/routes/mcpTools.ts
packages/api/src/routes/notifications.ts
packages/api/src/routes/oauth.ts
packages/api/src/routes/organizations.ts
packages/api/src/routes/recall.ts
packages/api/src/routes/workspaces.ts
packages/api/src/scripts/smokeEngine.ts
packages/api/src/scripts/smokeIndex.ts
packages/api/src/services/chunkService.ts
packages/api/src/services/documentService.ts
packages/api/src/services/emailService.ts
packages/api/src/services/embeddingService.ts
packages/api/src/services/entityExtractor.ts
packages/api/src/services/extractionService.ts
packages/api/src/services/jobService.ts
packages/api/src/services/notificationService.ts
packages/api/src/services/recallService.ts
packages/api/src/services/routingService.ts
packages/api/src/services/superCommitService.ts
packages/api/tsconfig.json
packages/dashboard/.env.example
packages/dashboard/index.html
packages/dashboard/package.json
packages/dashboard/postcss.config.js
packages/dashboard/public/blog/ai-memory-problem-is-team-problem/index.html
packages/dashboard/public/blog/best-mcp-memory-servers-for-teams/index.html
packages/dashboard/public/blog/claude-md-obsidian-workarounds/index.html
packages/dashboard/public/blog/context-cloud-vs-basic-memory/index.html
packages/dashboard/public/blog/context-cloud-vs-claude-mem/index.html
packages/dashboard/public/blog/context-cloud-vs-mem0/index.html
packages/dashboard/public/blog/context-cloud-vs-mempalace/index.html
packages/dashboard/public/blog/how-to-give-your-team-shared-ai-memory/index.html
packages/dashboard/public/blog/index.html
packages/dashboard/public/llms.txt
packages/dashboard/public/robots.txt
packages/dashboard/public/sitemap.xml
packages/dashboard/src/App.tsx
packages/dashboard/src/components/common/EmptyState.tsx
packages/dashboard/src/components/common/ErrorState.tsx
packages/dashboard/src/components/common/LoadingSkeleton.tsx
packages/dashboard/src/components/documents/DocumentList.tsx
packages/dashboard/src/components/documents/DocumentUpload.tsx
packages/dashboard/src/components/graph/edges.ts
packages/dashboard/src/components/graph/KnowledgeGraph.tsx
packages/dashboard/src/components/graph/ProcessingRing.tsx
packages/dashboard/src/components/graph/useForceLayout.ts
packages/dashboard/src/components/history/HistoryTimeline.tsx
packages/dashboard/src/components/jobs/ConversationViewer.tsx
packages/dashboard/src/components/kb/ChunkCard.tsx
packages/dashboard/src/components/kb/ChunkEditor.tsx
packages/dashboard/src/components/kb/ChunkList.tsx
packages/dashboard/src/components/kb/ContributorSummary.tsx
packages/dashboard/src/components/kb/CreateKbModal.tsx
packages/dashboard/src/components/kb/KBContextMenu.tsx
packages/dashboard/src/components/kb/KBPanel.tsx
packages/dashboard/src/components/layout/DashboardLayout.tsx
packages/dashboard/src/components/layout/Sidebar.tsx
packages/dashboard/src/components/layout/TopBar.tsx
packages/dashboard/src/components/layout/WorkspaceHeader.tsx
packages/dashboard/src/components/notifications/NotificationBell.tsx
packages/dashboard/src/components/onboarding/OnboardingConnectStep.tsx
packages/dashboard/src/components/onboarding/ToolSetupInstructions.tsx
packages/dashboard/src/components/settings/APIKeyManager.tsx
packages/dashboard/src/components/settings/MCPConfig.tsx
packages/dashboard/src/components/team/InviteForm.tsx
packages/dashboard/src/components/team/MemberList.tsx
packages/dashboard/src/components/workspace/DocumentsTab.tsx
packages/dashboard/src/components/workspace/HistoryTab.tsx
packages/dashboard/src/components/workspace/ListView.tsx
packages/dashboard/src/components/workspace/SettingsTab.tsx
packages/dashboard/src/components/workspace/TeamTab.tsx
packages/dashboard/src/hooks/useApiKeys.ts
packages/dashboard/src/hooks/useChunks.ts
packages/dashboard/src/hooks/useDocuments.ts
packages/dashboard/src/hooks/useEventListener.ts
packages/dashboard/src/hooks/useFetch.ts
packages/dashboard/src/hooks/useFocusRefetch.ts
packages/dashboard/src/hooks/useHistory.ts
packages/dashboard/src/hooks/useJobs.ts
packages/dashboard/src/hooks/useKnowledgeBases.ts
[46 more files omitted for size]
```

### Dependencies

- package.json: dotenv@^16.4.5, openai@^4.104.0, redis@^4.7.0
- packages/api/package.json: @clerk/backend@^1.21.0, @modelcontextprotocol/sdk@^1.29.0, @types/cors@^2.8.17, @types/express@^4.17.21, @types/multer@^1.4.12, @types/node@^22.0.0, @types/pdf-parse@^1.1.4, cors@^2.8.5, dotenv@^16.4.5, express@^4.21.0, jose@^5.9.6, mammoth@^1.8.0, mixpanel@^0.22.0, multer@^1.4.5-lts.1, openai@^4.104.0, pdf-parse@^1.1.1, redis@^4.7.0, resend@^4.1.0, svix@^1.45.1, tsx@^4.19.0, typescript@^5.6.0, zod@^3.23.8
- packages/dashboard/package.json: @clerk/clerk-react@^5.13.0, @types/mixpanel-browser@^2.66.0, @types/react@^18.3.12, @types/react-dom@^18.3.1, @vitejs/plugin-react@^4.3.4, autoprefixer@^10.4.20, clsx@^2.1.1, lucide-react@^0.460.0, mixpanel-browser@^2.78.0, postcss@^8.4.49, react@^18.3.1, react-dom@^18.3.1, react-router-dom@^6.28.0, tailwind-merge@^2.5.5, tailwindcss@^3.4.15, typescript@^5.6.0, vite@^5.4.11
- packages/mcp-client/package.json: @modelcontextprotocol/sdk@^1.29.0, @types/node@^22.0.0, tsx@^4.19.0, typescript@^5.6.0, zod@^3.23.8

### Recent commits (newest first)

- Add dashboard recall endpoint with hybrid-search explain bundle
- Remove private imports/ assets from repo and gitignore them
- Add phases 8-9: dashboard port + retrieval parity eval
- Add phases 5-7: extraction/routing/worker, surrounding product, auth
- Initial commit: ContextMaster — Redis-native rebuild of Context Cloud

## Key source files (fetched from GitHub, selected and truncated for size)

### docker-compose.yml

```yaml
# ContextMaster local dev infrastructure.
# Redis Stack = Redis + RediSearch (vector + full-text) + RedisJSON + Streams,
# the all-in data layer that replaces Supabase/Postgres for this rebuild.
services:
  redis:
    image: redis/redis-stack:7.4.0-v0
    container_name: contextmaster-redis
    ports:
      - "6379:6379"   # Redis protocol (app connects here via REDIS_URL)
      - "8001:8001"   # RedisInsight UI — open http://localhost:8001 to inspect
    volumes:
      - contextmaster-redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped

volumes:
  contextmaster-redis-data:

```

### package.json

```
{
  "name": "contextmaster",
  "private": true,
  "scripts": {
    "build": "pnpm -r build",
    "dev:api": "pnpm --filter @contextmaster/api dev",
    "dev:mcp": "pnpm --filter @contextmaster/mcp-client dev",
    "build:api": "pnpm --filter @contextmaster/api build",
    "build:mcp": "pnpm --filter @contextmaster/mcp-client build",
    "redis:up": "docker compose up -d redis",
    "redis:down": "docker compose down",
    "redis:logs": "docker compose logs -f redis",
    "redis:smoke": "pnpm --filter @contextmaster/api redis:smoke",
    "engine:smoke": "pnpm --filter @contextmaster/api engine:smoke",
    "lint": "pnpm -r lint"
  },
  "engines": {
    "node": ">=20"
  },
  "dependencies": {
    "dotenv": "^16.4.5",
    "openai": "^4.104.0",
    "redis": "^4.7.0"
  }
}

```

### packages/mcp-client/package.json

```
{
  "name": "@contextmaster/mcp-client",
  "version": "0.1.0",
  "description": "ContextMaster MCP client — persistent AI memory across sessions (Redis-native)",
  "type": "module",
  "bin": {
    "contextmaster-mcp": "dist/index.js"
  },
  "scripts": {
    "build": "tsc && chmod +x dist/index.js",
    "dev": "tsx src/index.ts",
    "lint": "tsc --noEmit"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "tsx": "^4.19.0",
    "typescript": "^5.6.0"
  },
  "files": [
    "dist"
  ]
}

```

### packages/dashboard/package.json

```
{
  "name": "@contextmaster/dashboard",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview",
    "lint": "tsc --noEmit"
  },
  "dependencies": {
    "@clerk/clerk-react": "^5.13.0",
    "clsx": "^2.1.1",
    "lucide-react": "^0.460.0",
    "mixpanel-browser": "^2.78.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^6.28.0",
    "tailwind-merge": "^2.5.5"
  },
  "devDependencies": {
    "@types/mixpanel-browser": "^2.66.0",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.4",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.4.49",
    "tailwindcss": "^3.4.15",
    "typescript": "^5.6.0",
    "vite": "^5.4.11"
  }
}

```

### packages/api/package.json

```
{
  "name": "@contextmaster/api",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "redis:smoke": "tsx src/scripts/smokeIndex.ts",
    "engine:smoke": "tsx src/scripts/smokeEngine.ts",
    "lint": "tsc --noEmit"
  },
  "dependencies": {
    "@clerk/backend": "^1.21.0",
    "@modelcontextprotocol/sdk": "^1.29.0",
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "express": "^4.21.0",
    "jose": "^5.9.6",
    "mammoth": "^1.8.0",
    "mixpanel": "^0.22.0",
    "multer": "^1.4.5-lts.1",
    "openai": "^4.104.0",
    "pdf-parse": "^1.1.1",
    "redis": "^4.7.0",
    "resend": "^4.1.0",
    "svix": "^1.45.1",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/cors": "^2.8.17",
    "@types/express": "^4.17.21",
    "@types/multer": "^1.4.12",
    "@types/node": "^22.0.0",
    "@types/pdf-parse": "^1.1.4",
    "tsx": "^4.19.0",
    "typescript": "^5.6.0"
  }
}

```

### packages/dashboard/src/App.tsx

```typescript
import { Navigate, Route, Routes } from "react-router-dom";
import { DashboardLayout } from "./components/layout/DashboardLayout";
import { HomePage } from "./pages/HomePage";
import { OnboardingPage } from "./pages/OnboardingPage";
import { WorkspacePage } from "./pages/WorkspacePage";
import { AcceptInvitePage } from "./pages/AcceptInvitePage";
import { OrganizationsPage } from "./pages/OrganizationsPage";
import { OrganizationPage } from "./pages/OrganizationPage";
import { OAuthAuthorizePage } from "./pages/OAuthAuthorizePage";
import { SettingsPage } from "./pages/SettingsPage";
import { HelpPage } from "./pages/HelpPage";
import { PrivacyPolicyPage } from "./pages/PrivacyPolicyPage";

export function App() {
  return (
    <Routes>
      <Route element={<DashboardLayout />}>
        <Route path="/" element={<HomePage />} />
        <Route path="/workspace/:id" element={<WorkspacePage />} />
        <Route path="/workspace/:id/:tab" element={<WorkspacePage />} />
        <Route path="/organizations" element={<OrganizationsPage />} />
        <Route path="/organizations/:id" element={<OrganizationPage />} />
        <Route path="/settings" element={<SettingsPage />} />
        <Route path="/help" element={<HelpPage />} />
      </Route>
      <Route path="/onboarding" element={<OnboardingPage />} />
      <Route path="/invite/:token" element={<AcceptInvitePage />} />
      <Route path="/oauth/authorize" element={<OAuthAuthorizePage />} />
      <Route path="/privacy" element={<PrivacyPolicyPage />} />
      <Route path="*" element={<Navigate to="/" replace />} />
    </Routes>
  );
}

```

### packages/dashboard/src/main.tsx

```typescript
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { ClerkProvider, SignedIn, SignedOut, useAuth } from "@clerk/clerk-react";
import { App } from "./App";
import { AcceptInvitePage } from "./pages/AcceptInvitePage";
import { LandingPage } from "./pages/LandingPage";
import { PrivacyPolicyPage } from "./pages/PrivacyPolicyPage";
import { setTokenGetter } from "./lib/auth";
import { AUTH_BYPASS_ENABLED, CLERK_PUBLISHABLE_KEY } from "./lib/constants";
import "./styles/globals.css";

function ClerkTokenBridge() {
  const { getToken } = useAuth();
  setTokenGetter(async () => {
    const token = await getToken();
    return token ?? null;
  });
  return null;
}

function AppOrLanding() {
  return (
    <>
      <SignedIn>
        <App />
      </SignedIn>
      <SignedOut>
        <LandingPage />
      </SignedOut>
    </>
  );
}

function Root() {
  if (AUTH_BYPASS_ENABLED) {
    return (
      <BrowserRouter>
        <App />
      </BrowserRouter>
    );
  }
  if (!CLERK_PUBLISHABLE_KEY) {
    return (
      <div className="flex h-screen w-screen items-center justify-center p-8 text-center">
        <div className="max-w-md text-ink-700">
          <div className="text-lg font-semibold mb-2 text-ink-900">
            Clerk not configured
          </div>
          <p className="text-[13px] leading-relaxed">
            Set <code className="bg-cream-200 px-1 rounded">VITE_CLERK_PUBLISHABLE_KEY</code> in
            <code className="bg-cream-200 px-1 rounded ml-1">.env</code>, or
            set <code className="bg-cream-200 px-1 rounded">VITE_AUTH_BYPASS=true</code> for dev mode.
          </p>
        </div>
      </div>
    );
  }
  return (
    <ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY}>
      <ClerkTokenBridge />
      <BrowserRouter>
        <Routes>
          {/* Invite routes work both signed-in and signed-out so users can
              create an account from the invite link. */}
          <Route path="/invite/:token" element={<AcceptInvitePage />} />
          <Route path="/privacy" element={<PrivacyPolicyPage />} />
          <Route path="*" element={<AppOrLanding />} />
        </Routes>
      </BrowserRouter>
    </ClerkProvider>
  );
}

const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("#root element missing");
createRoot(rootElement).render(
  <StrictMode>
    <Root />
  </StrictMode>
);

```

### packages/api/src/index.ts

```typescript
import "./loadEnv.js";

import express from "express";
import cors from "cors";
import { getRedis, connectRedis, resolveRedisUrl, redactUrl } from "./lib/redis.js";
import { ensureRedisInfra, listIndexes } from "./lib/indexes.js";
import { idx } from "./lib/keys.js";
import { ensureDevEnvironment } from "./lib/bootstrap.js";
import { apiKeyAuth } from "./middleware/apiKeyAuth.js";
import { clerkAuth } from "./middleware/clerkAuth.js";
import { mcpRouter } from "./routes/mcp.js";
import { mcpProtocolHandler } from "./routes/mcpProtocol.js";
import { mcpSSEHandler } from "./routes/mcpSSE.js";
import { authRouter } from "./routes/auth.js";
import { clerkWebhookRouter } from "./routes/clerkWebhook.js";
import { workspacesRouter, publicInvitesRouter } from "./routes/workspaces.js";
import { organizationsRouter } from "./routes/organizations.js";
import { notificationsRouter } from "./routes/notifications.js";
import { billingRouter } from "./routes/billing.js";
import { recallRouter } from "./routes/recall.js";
import { oauthRouter } from "./routes/oauth.js";
import { startWorker } from "./services/jobService.js";

const PORT = Number(process.env.PORT ?? 3001);
const PUBLIC_API_URL = process.env.PUBLIC_API_URL ?? `http://localhost:${PORT}`;
const PUBLIC_DASHBOARD_URL =
  process.env.PUBLIC_DASHBOARD_URL ?? "http://localhost:3000";
const redisUrl = resolveRedisUrl();
const redis = getRedis();

// (Re)build indexes + the jobs consumer group, then seed the dev environment,
// whenever Redis becomes ready — so a fresh container or reconnect always has
// the infra and the AUTH_BYPASS user/workspace/api-key in place.
redis.on("ready", () => {
  ensureRedisInfra(redis)
    .then(() => ensureDevEnvironment(redis))
    .catch((err) => console.error("[redis] infra bootstrap failed:", (err as Error).message));
});

const app = express();
app.use(
  cors({
    origin: "*",
    exposedHeaders: ["Mcp-Session-Id"],
    allowedHeaders: ["Content-Type", "Authorization", "Mcp-Session-Id", "Last-Event-ID", "Accept"],
  })
);

// Clerk webhook — MUST be mounted before express.json() and clerkAuth so svix
// can verify the raw request body. Webhooks authenticate via the svix
// signature, not a Clerk JWT.
app.use("/api/auth", clerkWebhookRouter);

// OAuth 2.0 authorization-server metadata (RFC 8414). Public — no auth.
// OAuth-aware MCP clients read this from the WWW-Authenticate challenge to
// discover our authorize/token endpoints without manual configuration.
app.get("/.well-known/oauth-authorization-server", (_req, res) => {
  res.json({
    issuer: PUBLIC_API_URL,
    authorization_endpoint: `${PUBLIC_DASHBOARD_URL}/oauth/authorize`,
    token_endpoint: `${PUBLIC_API_URL}/oauth/token`,
    registration_endpoint: `${PUBLIC_API_URL}/oauth/register`,
    response_types_supported: ["code"],
    grant_types_supported: ["authorization_code"],
    code_challenge_methods_supported: ["S256"],
    token_endpoint_auth_methods_supported: ["client_secret_post", "none"],
    scopes_supported: ["mcp"],
  });
});

// OAuth 2.0 protected-resource metadata (RFC 9728).
app.get("/.well-known/oauth-protected-resource", (_req, res) => {
  res.json({
    resource: PUBLIC_API_URL,
    authorization_servers: [PUBLIC_API_URL],
    scopes_supported: ["mcp"],
    bearer_methods_supported: ["header"],
  });
});

// OAuth routes — manage their own auth + body parsing (Clerk for
// /authorize/callback, client_secret/PKCE for /token, none for /register), so
// they're mounted before the global express.json().
app.use("/oauth", oauthRouter);

app.use(express.json({ limit: "10mb" }));

app.get("/health", async (_req, res) => {
  let redisStatus = "down";
  let redisLatencyMs: number | null = null;
  let indexes: { chunks: boolean; kbs: boolean } | null = null;

  if (redis.isReady) {
    try {
      const start = Date.now();
      const pong = await redis.ping();
      redisLatencyMs = Date.now() - start;
      redisStatus = pong === "PONG" ? "ok" : "unexpected";
      const present = await listIndexes(redis);
      indexes = { chunks: present.includes(idx.chunks), kbs: present.includes(idx.kbs) };
    } catch (err) {
      redisStatus = `error: ${(err as Error).message}`;
    }
  }

  res.json({
    service: "contextmaster-api",
    status: "ok",
    redis: { status: redisStatus, latencyMs: redisLatencyMs, url: redactUrl(redisUrl), indexes },
    authBypass: process.env.AUTH_BYPASS === "true",
    serverSideExtraction: process.env.SERVER_SIDE_EXTRACTION === "true",
    timestamp: new Date().toISOString(),
  });
});

// MCP transports + REST authenticate via API key (apiKeyAuth) — used by the
// stdio mcp-client and the remote HTTP/SSE connectors. AUTH_BYPASS-aware in dev.
app.all("/mcp/protocol", apiKeyAuth, mcpProtocolHandler);
app.get("/mcp/sse", apiKeyAuth, mcpSSEHandler.get);
app.post("/mcp/sse", apiKeyAuth, mcpSSEHandler.post);
app.use("/mcp", apiKeyAuth, mcpRouter);

// ---- Dashboard-facing REST API (Clerk JWT auth; AUTH_BYPASS-aware in dev) ----
// Public invite preview is mounted before auth so signed-out users can see an
// invite.
app.use("/api", publicInvitesRouter);
app.use("/api/auth", clerkAuth, authRouter);
app.use("/api/organizations", clerkAuth, organizationsRouter);
app.use("/api/notifications", clerkAuth, notificationsRouter);
app.use("/api/billing", clerkAuth, billingRouter);
app.use("/api/recall", clerkAuth, recallRouter);
app.use("/api", clerkAuth, workspacesRouter);

// Start the HTTP server immediately; connect to Redis in the background so the
// API boots (and /health responds) even when Redis isn't up yet.
app.listen(PORT, () => {
  console.log(`[api] ContextMaster API listening on http://localhost:${PORT}`);
  console.log(`[api] health:   http://localhost:${PORT}/health`);
  console.log(`[api] MCP REST: http://localhost:${PORT}/mcp/*`);
  console.log(`[api] MCP-RPC:  http://localhost:${PORT}/mcp/protocol  (Streamable HTTP)`);
  console.log(`[api] MCP-SSE:  http://localhost:${PORT}/mcp/sse        (SSE)`);
  co
[truncated — 800 more characters]
```

### packages/mcp-client/src/index.ts

```typescript
#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { apiRequest } from "./apiClient.js";
import { detectPlatform } from "./platformDetector.js";
import { parseJsonlFile } from "./jsonlParser.js";

const server = new McpServer(
  { name: "contextmaster", version: "0.1.0" },
  {
    instructions: `You have cntxt connected — a persistent memory system with the user's knowledge bases containing decisions, findings, conventions, and project context from prior sessions.

DEFAULT BEHAVIOR: Call check_memory on your FIRST substantive response in every conversation. This is not optional — treat it like checking your memory before speaking. The only exceptions are purely factual questions with no personal context ('what is a REST API', 'how do I center a div'). If there is ANY chance the user has relevant prior context, check first.

After check_memory returns, if any knowledge base is relevant to what the user is discussing, call search_memory to get specifics. Use the results to ground your response — don't mention that you checked, just use the knowledge naturally.

At natural stopping points (decisions made, work completed, session ending), proactively suggest saving: 'Want me to save our progress?' The user says 'save this' or agrees, then call save_memory.

IMPORTANT: You are not a separate tool the user invokes. You are the user's memory. Act like it — check automatically, recall silently, suggest saving naturally.`,
  }
);

// ---- Tool 1: check_memory ----
server.tool(
  "check_memory",
  `Returns the table of contents for the user's persistent memory: most_recent_kb (the KB the user was last working in — use for unscoped "continue"/"where did we leave off" phrases), workspaces[].knowledge_bases (all KBs, newest-updated first), and shared_knowledge_bases. Each KB carries name, description, last_session_summary, last_updated, chunk_count.

Call this as a PRECURSOR to any retrieval — the moment you need to know what the user is talking about and the answer isn't in the current conversation. Trigger phrases: 'where did we leave off', 'what were we working on', 'continue from last time', 'catch me up', 'what did we decide about...', or any reference to past work by name.

DO NOT call ceremonially on every start, for self-contained factual questions, or when you already have the KB IDs. Don't dump the raw response to the user — use it to ground your reply.`,
  {},
  async () => {
    try {
      const data = await apiRequest({ method: "GET", path: "/mcp/context" });
      return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
    } catch (err: any) {
      return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
    }
  }
);

// ---- Tool 2: search_memory ----
server.tool(
  "search_memory",
  `Search your knowledge bases. Specify knowledge_base_ids or knowledge_base to narrow, or omit for automatic semantic routing across the most relevant KBs.

Use broad search by default; only narrow when the user explicitly names a project, KB, or workspace. Results tagged [from knowledge base] are existing context — do NOT re-extract them when committing later. When continuing work that spans topics, make 2-3 TARGETED searches with different queries rather than one broad search.`,
  {
    query: z.string().describe("Natural language description of what to retrieve"),
    knowledge_base_ids: z.array(z.string()).optional().describe("IDs of KBs to search (from check_memory)."),
    knowledge_base: z.string().optional().describe("Name of a KB to search — the system resolves it."),
    workspace: z.string().optional().describe("Workspace name — searches all KBs in it."),
    max_results: z.number().optional().default(16).describe("Maximum number of chunks to return"),
    chunk_types: z
      .array(z.string())
      .optional()
      .describe("Optional filter: decision, finding, convention, state, question, reference"),
  },
  async (params) => {
    try {
      const data = await apiRequest({
        method: "POST",
        path: "/mcp/recall",
        body: {
          query: params.query,
          knowledge_base_ids: params.knowledge_base_ids,
          knowledge_base: params.knowledge_base,
          workspace: params.workspace,
          max_results: params.max_results,
          chunk_types: params.chunk_types,
        },
      });

      const taggedChunks = (data.chunks ?? []).map((chunk: any) => {
        let content = `[from knowledge base: ${chunk.knowledge_base_name}] ${chunk.content}`;
        if (Array.isArray(chunk.linked_chunks) && chunk.linked_chunks.length > 0) {
          content += `\n  Supporting context:`;
          for (const linked of chunk.linked_chunks) content += `\n  - ${linked.content}`;
        }
        return { ...chunk, content };
      });

      return { content: [{ type: "text", text: JSON.stringify({ chunks: taggedChunks }, null, 2) }] };
    } catch (err: any) {
      return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
    }
  }
);

// ---- Tool 3: save_memory (always registered) ----
server.tool(
  "save_memory",
  `Save structured knowledge chunks to Context Cloud. Prefer save_session — it handles extraction automatically. Use save_memory only if you've already structured the data into chunks.

Pick an existing knowledge_base_id when the topic matches one (from check_memory), or set it null and pass new_knowledge_base for a DISTINCT ongoing endeavor. Preserve specifics verbatim (proper nouns, numbers, URLs, paths, identifiers). Do NOT re-extract anything tagged [from knowledge base]. Use chunk types decision/finding/convention/state/question/reference/context. Add a kebab-case topic_key to state/decision chunks that have a single current value that can change later. Write a detailed session_summary that mentions every proper noun, num
[truncated — 9672 more characters]
```

### pnpm-workspace.yaml

```yaml
packages:
  - 'packages/*'

allowBuilds:
  '@clerk/shared': set this to true or false
  esbuild: set this to true or false

# pnpm v10+ blocks postinstall by default, which breaks esbuild (needs its
# native binary) and Clerk (postinstall wiring). Mirrors the reference repo.
onlyBuiltDependencies:
  - esbuild
  - '@clerk/shared'

```

[141 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]