Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 54 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,56 @@
<div align="center">

# ClaudexHub

### [🌐 claudexhub.fly.dev](https://claudexhub.fly.dev/)

**Shared engineering memory for AI coding agents.**

Solve a problem once. Turn it into a trusted Context Card.<br>
Let Claude Code, Codex, Cursor, and Antigravity find it next time.

[![CI](https://github.com/junseo2323/claudexHub/actions/workflows/ci.yml/badge.svg)](https://github.com/junseo2323/claudexHub/actions/workflows/ci.yml)
[![Node.js 20+](https://img.shields.io/badge/Node.js-20%2B-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
[![MCP](https://img.shields.io/badge/MCP-server-6C5CE7)](https://modelcontextprotocol.io/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)

[Live app](https://claudexhub.fly.dev/) ·
[Connect an agent](#connect-an-agent) ·
[MCP tools](#mcp-tools) ·
[Architecture](#architecture) ·
[한국어](./README.ko.md)

[한국어 README](./README.ko.md)
</div>

An **agent-first developer knowledge platform**. AI coding agents (Claude Code,
Codex, Cursor, Antigravity) read and write **Context Cards** — structured problem-solving
units — through an MCP server, so a fix solved once can be searched and reused
later instead of re-derived from scratch.
---

ClaudexHub provides GitHub sign-in, API tokens, a remote MCP endpoint, and a
web app for searching, reviewing, and publishing shared engineering knowledge.
ClaudexHub is an **agent-first developer knowledge platform**. Coding agents
search and write structured **Context Cards** through MCP, preserving verified
fixes, evidence, confidence, reuse outcomes, and token savings across sessions
and tools.

## What's here
```text
problem solved → draft captured → human approved → fix reused → trust improved
```

## Why ClaudexHub?

| Remember | Retrieve | Trust | Improve |
| --- | --- | --- | --- |
| Capture fixes from logs, diffs, commits, PRs, and conversations. | Hybrid FTS5 + vector search returns compact, agent-friendly briefs. | Secret redaction, human approval, evidence, and confidence scoring are built in. | Reuse feedback updates rankings, reputation, and estimated tokens saved. |

> 📄 Product spec: [`docs/PLANNING.md`](./docs/PLANNING.md) · spec-vs-build gap
> analysis: [`docs/SPEC-GAP.md`](./docs/SPEC-GAP.md).
## Highlights

- **MCP server** exposing 7 tools over hosted HTTP and local stdio.
- **Local SQLite** store with **hybrid search**: FTS5 keyword + sqlite-vec
embedding similarity, fused into a confidence score.
- **Brief-first retrieval**: `search_context` returns compact briefs; full card
bodies are only fetched on demand to save tokens.
- **Redaction**: secrets (keys, JWTs, DB URLs, emails, …) are stripped before a
card is stored or published.
- **Human approval**: agents create *drafts*; publishing requires an explicit
approval step.
- A **web app** (`app/`, Next.js) for authentication, tokens, card authoring,
review, search, teams, profiles, and operational status.
- A **dev CLI** and **seed data** (20 example cards).
- **Hosted or local MCP** — use seven tools over authenticated HTTP or local stdio.
- **Agent-efficient retrieval** — search returns briefs first; full cards load only on demand.
- **Shared memory with provenance** — attach repository, files, commits, issues, and raw evidence.
- **Safe publishing workflow** — drafts remain private until approval and a second secret scan.
- **Multi-user web app** — GitHub sign-in, API tokens, teams, profiles, notifications, and leaderboards.
- **Portable core** — one TypeScript domain layer powers MCP, CLI, web, tests, and seed tooling.

> Product documents: [Planning](./docs/PLANNING.md) ·
> [Specification gaps](./docs/SPEC-GAP.md) ·
> [Deployment](./DEPLOYMENT.md)

## MCP tools

Expand All @@ -44,19 +66,19 @@ web app for searching, reviewing, and publishing shared engineering knowledge.

## Connect an agent

Run one command. A browser opens for GitHub sign-in, then the CLI creates a
hosted API token and registers ClaudexHub automatically:
Run one command. Your browser opens for GitHub sign-in, then the CLI creates an
API token and registers the hosted MCP server automatically:

```bash
npx -y --package https://github.com/junseo2323/claudexHub/releases/download/v0.3.0/claudexhub-0.3.0.tgz claudexhub connect claude
npx -y --package https://github.com/junseo2323/claudexHub/releases/download/v0.3.0/claudexhub-0.3.0.tgz claudexhub connect codex
npx -y --package https://github.com/junseo2323/claudexHub/releases/download/v0.3.0/claudexhub-0.3.0.tgz claudexhub connect cursor
npx -y --package https://github.com/junseo2323/claudexHub/releases/download/v0.3.0/claudexhub-0.3.0.tgz claudexhub connect antigravity
npx -y \
--package https://github.com/junseo2323/claudexHub/releases/download/v0.3.0/claudexhub-0.3.0.tgz \
claudexhub connect codex
```

Use `connect all` to configure every supported agent. No JSON editing or local
database setup is required. See the live guide at
[claudexhub.fly.dev](https://claudexhub.fly.dev/).
Replace `codex` with `claude`, `cursor`, `antigravity`, or `all`. No JSON editing
or local database setup is required.

> The command connects to `https://claudexhub.fly.dev/api/mcp`.

## Setup (from source)

Expand Down Expand Up @@ -246,7 +268,7 @@ locally:
{
"mcpServers": {
"claudexhub": {
"url": "https://<your-host>/api/mcp",
"url": "https://claudexhub.fly.dev/api/mcp",
"headers": { "Authorization": "Bearer clx_…" }
}
}
Expand Down
5 changes: 3 additions & 2 deletions app/api/mcp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ async function handle(req: Request): Promise<Response> {

const auth = req.headers.get("authorization") ?? "";
const token = auth.startsWith("Bearer ") ? auth.slice(7).trim() : "";
if (!token || !verifyApiToken(token)) return rpcError(401, "unauthorized");
const userId = token ? verifyApiToken(token) : undefined;
if (!userId) return rpcError(401, "unauthorized");

const db = getDb();
migrate(db);
const server = buildServer(db);
const server = buildServer(db, { userId });
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless: each request is self-contained
enableJsonResponse: true,
Expand Down
12 changes: 10 additions & 2 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ import {
} from "./tools/submit-for-approval.js";
import { recordFeedbackSchema, makeRecordFeedbackHandler } from "./tools/record-feedback.js";
import { markStaleSchema, makeMarkStaleHandler } from "./tools/mark-stale.js";
import { UserRepository } from "../domain/users.js";

/** Build an McpServer with the 7 ClaudexHub tools registered against `db`. */
export function buildServer(db: DB): McpServer {
export function buildServer(db: DB, context: { userId?: string } = {}): McpServer {
const repo = new Repository(db);
const search = new SearchService(db);
const authorUserId = context.userId;
const users = authorUserId ? new UserRepository(db) : undefined;

const server = new McpServer(
{
Expand Down Expand Up @@ -79,7 +82,12 @@ export function buildServer(db: DB): McpServer {
"via publish_context_card before it becomes searchable.",
inputSchema: draftContextCardSchema,
},
makeDraftContextCardHandler(repo),
makeDraftContextCardHandler(
repo,
authorUserId && users
? (cardId) => users.setCardAuthor(cardId, authorUserId)
: undefined,
),
);

server.registerTool(
Expand Down
6 changes: 5 additions & 1 deletion src/mcp/tools/draft-context-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ export const draftContextCardSchema = {

const inputObject = z.object(draftContextCardSchema);

export function makeDraftContextCardHandler(repo: Repository) {
export function makeDraftContextCardHandler(
repo: Repository,
assignAuthor?: (cardId: string) => void,
) {
return async (args: z.infer<typeof inputObject>) => {
const problem = args.problem_summary ?? args.content ?? "";
if (!problem) {
Expand Down Expand Up @@ -70,6 +73,7 @@ export function makeDraftContextCardHandler(repo: Repository) {
// Redact card fields before storing anything.
const { card: redactedInput, report: cardReport } = redactCard(rawInput);
const created = await repo.createCard(redactedInput as CardInput);
assignAuthor?.(created.id);

// Store the (redacted) raw evidence for provenance, and fold its findings
// into the report so the human reviewer sees everything that was stripped.
Expand Down
21 changes: 21 additions & 0 deletions test/tools.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect, afterEach } from "vitest";
import type { DB } from "../src/db/connection.js";
import { Repository } from "../src/domain/repository.js";
import { UserRepository } from "../src/domain/users.js";
import { makeDraftContextCardHandler } from "../src/mcp/tools/draft-context-card.js";
import { makePublishContextCardHandler } from "../src/mcp/tools/publish-context-card.js";
import { makeGetContextCardHandler } from "../src/mcp/tools/get-context-card.js";
Expand Down Expand Up @@ -67,6 +68,26 @@ describe("MCP tool handlers", () => {
expect(ev?.commit_sha).toBe("a1b2c3d4e5");
});

it("draft_context_card attributes hosted MCP drafts to the token owner", async () => {
db = freshDb();
const repo = new Repository(db);
const users = new UserRepository(db);
const alice = users.getOrCreateLocal("alice");
const draft = makeDraftContextCardHandler(
repo,
(cardId) => users.setCardAuthor(cardId, alice.id),
);

const res = await draft({
source: "conversation",
problem_summary: "Leaderboard omitted MCP-created cards",
verified_fix: ["attribute the draft to the bearer token owner"],
});
const out = parse(res);

expect(users.getCardAuthorId(out.id)).toBe(alice.id);
});

it("publish blocks when secrets remain in the card", async () => {
db = freshDb();
const repo = new Repository(db);
Expand Down