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
3 changes: 2 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@
"@company-brain/utils": "workspace:*",
"@company-brain/workflows": "workspace:*",
"@fastify/compress": "^8.0.1",
"@fastify/websocket": "^11.0.1",
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.0.1",
"@fastify/helmet": "^13.0.1",
"@fastify/multipart": "^10.1.0",
"@fastify/rate-limit": "^10.2.2",
"@fastify/swagger": "^9.5.0",
"@fastify/swagger-ui": "^5.2.2",
"@fastify/websocket": "^11.0.1",
"@modelcontextprotocol/sdk": "^1.30.0",
"@prisma/client": "^6.6.0",
"@qdrant/js-client-rest": "^1.14.0",
"@temporalio/client": "^1.20.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
-- CreateEnum
CREATE TYPE "McpServerStatus" AS ENUM ('ACTIVE', 'DISABLED');

-- CreateEnum
CREATE TYPE "McpVisibility" AS ENUM ('WORKSPACE', 'PRIVATE', 'SHARED', 'PUBLIC');

-- CreateTable
CREATE TABLE "mcp_servers" (
"id" UUID NOT NULL,
"organizationId" UUID NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"scopeConfig" JSONB NOT NULL DEFAULT '{"mode":"workspace"}',
"tools" TEXT[],
"prompt" TEXT,
"visibility" "McpVisibility" NOT NULL DEFAULT 'WORKSPACE',
"status" "McpServerStatus" NOT NULL DEFAULT 'ACTIVE',
"createdById" UUID,
"ownerId" UUID,
"lastModifiedById" UUID,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),

CONSTRAINT "mcp_servers_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "mcp_api_keys" (
"id" UUID NOT NULL,
"mcpServerId" UUID NOT NULL,
"name" TEXT NOT NULL,
"prefix" TEXT NOT NULL,
"keyHash" TEXT NOT NULL,
"readOnly" BOOLEAN NOT NULL DEFAULT true,
"lastUsedAt" TIMESTAMP(3),
"expiresAt" TIMESTAMP(3),
"revokedAt" TIMESTAMP(3),
"createdById" UUID,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "mcp_api_keys_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "mcp_connections" (
"id" UUID NOT NULL,
"mcpServerId" UUID NOT NULL,
"clientName" TEXT,
"clientVersion" TEXT,
"lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "mcp_connections_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "mcp_servers_organizationId_idx" ON "mcp_servers"("organizationId");

-- CreateIndex
CREATE INDEX "mcp_servers_organizationId_status_idx" ON "mcp_servers"("organizationId", "status");

-- CreateIndex
CREATE UNIQUE INDEX "mcp_api_keys_prefix_key" ON "mcp_api_keys"("prefix");

-- CreateIndex
CREATE UNIQUE INDEX "mcp_api_keys_keyHash_key" ON "mcp_api_keys"("keyHash");

-- CreateIndex
CREATE INDEX "mcp_api_keys_mcpServerId_idx" ON "mcp_api_keys"("mcpServerId");

-- CreateIndex
CREATE INDEX "mcp_connections_mcpServerId_idx" ON "mcp_connections"("mcpServerId");

-- CreateIndex
CREATE UNIQUE INDEX "mcp_connections_mcpServerId_clientName_key" ON "mcp_connections"("mcpServerId", "clientName");

-- AddForeignKey
ALTER TABLE "mcp_api_keys" ADD CONSTRAINT "mcp_api_keys_mcpServerId_fkey" FOREIGN KEY ("mcpServerId") REFERENCES "mcp_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "mcp_connections" ADD CONSTRAINT "mcp_connections_mcpServerId_fkey" FOREIGN KEY ("mcpServerId") REFERENCES "mcp_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
104 changes: 104 additions & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,110 @@ model AuditLog {
@@map("audit_logs")
}

// ────────────────────────────────────────────────────────────────
// MCP (Model Context Protocol) — expose the Company Brain as a
// permission-aware, always-live context server for external AI tools
// (Claude Desktop, Cursor, VS Code, …). An organization owns one or
// more MCP servers; each exposes a configurable slice of collective
// knowledge and is reached over Streamable HTTP with a hashed API key.
// Scalar org/user FKs (additive, no cross-model relations) per repo
// convention; real relations only among the MCP models themselves.
// ────────────────────────────────────────────────────────────────

enum McpServerStatus {
ACTIVE
DISABLED
}

enum McpVisibility {
WORKSPACE // every org member can see/use (v1 default)
PRIVATE // creator only (reserved — not enforced in v1)
SHARED // selected members (reserved)
PUBLIC // reserved future
}

model McpServer {
id String @id @default(uuid()) @db.Uuid

organizationId String @db.Uuid

name String
description String?

// { mode: 'workspace' } exposes all org knowledge.
// { mode: 'scoped', projectIds?, documentIds?, meetingIds?, memberIds? }
// restricts to a provable slice (fail-closed at retrieval time).
scopeConfig Json @default("{\"mode\":\"workspace\"}")

// Enabled tool names (subset of the server's tool catalog).
tools String[]
// Optional system prompt surfaced to connecting clients.
prompt String?

visibility McpVisibility @default(WORKSPACE)
status McpServerStatus @default(ACTIVE)

// Accountability — user ids (scalar, no relation per convention).
createdById String? @db.Uuid
ownerId String? @db.Uuid
lastModifiedById String? @db.Uuid

apiKeys McpApiKey[]
connections McpConnection[]

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?

@@index([organizationId])
@@index([organizationId, status])
@@map("mcp_servers")
}

model McpApiKey {
id String @id @default(uuid()) @db.Uuid

mcpServerId String @db.Uuid
mcpServer McpServer @relation(fields: [mcpServerId], references: [id], onDelete: Cascade)

name String

// First characters of the key, shown in UIs for identification.
prefix String @unique
keyHash String @unique
readOnly Boolean @default(true)

lastUsedAt DateTime?
expiresAt DateTime?
revokedAt DateTime?

createdById String? @db.Uuid

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([mcpServerId])
@@map("mcp_api_keys")
}

model McpConnection {
id String @id @default(uuid()) @db.Uuid

mcpServerId String @db.Uuid
mcpServer McpServer @relation(fields: [mcpServerId], references: [id], onDelete: Cascade)

// Client identity from the MCP initialize request (e.g. "Claude Desktop").
clientName String?
clientVersion String?

lastSeenAt DateTime @default(now())
createdAt DateTime @default(now())

@@unique([mcpServerId, clientName])
@@index([mcpServerId])
@@map("mcp_connections")
}

// ────────────────────────────────────────────────────────────────
// Phase 1 — Knowledge Brain
// Documents, versions, chunks, embeddings, folders, tags, sources
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import liveRoutes from './modules/live/live.routes.js';
import activityRoutes from './modules/activity/activity.routes.js';
import workspaceRoutes from './modules/workspace/workspace.routes.js';
import llmSettingsRoutes from './modules/llm/llm-settings.routes.js';
import mcpRoutes from './modules/mcp/mcp.routes.js';

/**
* Builds a fully configured Fastify instance. Kept separate from the
Expand Down Expand Up @@ -90,6 +91,7 @@ export async function buildApp(): Promise<FastifyInstance> {
await app.register(activityRoutes, { prefix: '/api/v1/activity' });
await app.register(workspaceRoutes, { prefix: '/api/v1/workspace' });
await app.register(llmSettingsRoutes, { prefix: '/api/v1/llm' });
await app.register(mcpRoutes, { prefix: '/api/v1' });

return app;
}
29 changes: 29 additions & 0 deletions apps/api/src/modules/mcp/mcp.keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createHash, randomBytes } from 'node:crypto';

/**
* MCP API keys. The full secret is shown to the user exactly once at
* creation/rotation; only its SHA-256 hash and a short human-readable prefix
* are persisted — mirroring the platform's existing `APIKey` model.
*/

const KEY_BYTES = 24;
const KEY_PREFIX = 'mcp_';

export interface GeneratedKey {
/** Full secret — returned to the caller exactly once, never stored. */
secret: string;
/** Human-visible identifier (also the leading chars of the secret). */
prefix: string;
/** SHA-256 of the full secret — the only form persisted. */
keyHash: string;
}

export function generateKey(): GeneratedKey {
const secret = KEY_PREFIX + randomBytes(KEY_BYTES).toString('base64url');
return { secret, prefix: secret.slice(0, 14), keyHash: hashKey(secret) };
}

/** Hash a presented secret for constant-time lookup against `keyHash`. */
export function hashKey(secret: string): string {
return createHash('sha256').update(secret).digest('hex');
}
Loading
Loading