Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
f75b46b
mit-salvage: reintroduce _graph edges accelerator + admin lifecycle p…
voarsh Feb 9, 2026
d4be80a
Admin: fix option to clear indexing caches
voarsh Feb 9, 2026
2e59455
fix(upload-client): stop dev-workspace recursion in dev-remote watch …
voarsh Feb 9, 2026
881f1f4
fix(indexer): harden graph edge backfill + align upload client ignore…
voarsh Feb 9, 2026
7961a72
chore: harden graph-edge ops, cache uploader excludes, and sync helpers
voarsh Feb 9, 2026
2476d6c
collection_admin: Fix missing logging
voarsh Feb 13, 2026
b43054b
Adds debug mode to repo search
voarsh Feb 13, 2026
843d79f
bridge: make MCP list timeouts configurable and gate OAuth metadata f…
voarsh Feb 13, 2026
2f14eff
fix(mcp): correct template dedupe uri source and clean debug field ha…
voarsh Feb 13, 2026
a8656de
vscode-ext: Adds bundled MCP bridge mode
voarsh Feb 13, 2026
6cecc26
Add back Claude Code workflow for GH
voarsh Feb 14, 2026
ec69b2b
Improves upload client and code search handling
voarsh Feb 14, 2026
ba3d336
Prompts for venv creation when auto-detection fails
voarsh Feb 14, 2026
a620125
Updates session defaults on ID change
voarsh Mar 2, 2026
7ed96d9
refactor(bridge): consolidate session defaults sync
voarsh Mar 6, 2026
036c677
fix(search): change `under` filter to recursive subtree scope
voarsh Mar 7, 2026
ba1e9c2
refactor(ingest): add async git history processing and structured log…
voarsh Mar 7, 2026
d30e1c4
fix(vscode-uploader): restore watch startup after successful auto for…
voarsh Mar 7, 2026
24b7c3f
refactor(ingest): improve logging practices and thread safety
voarsh Mar 7, 2026
fb560c1
fix(uploader): restore incremental sync cache and reduce Windows Pyth…
voarsh Mar 7, 2026
8c05f45
feat(upload): add hash-based deduplication and processing status trac…
voarsh Mar 7, 2026
366b6f4
feat(upload): cleanup ignored cached paths and prune empty directorie…
voarsh Mar 7, 2026
0a380b9
feat(upload): add interval-based empty dir sweep and fix force sync i…
voarsh Mar 7, 2026
168f22f
feat(upload): add plan/apply workflow for delta uploads
voarsh Mar 7, 2026
ca32c5a
fix(upload,watch): align cache state with confirmed uploads and trim …
voarsh Mar 8, 2026
673ad7e
fix(ingest,watch): tolerate line shifts and reduce redundant reproces…
voarsh Mar 8, 2026
6f243ec
feat(vscode): extend MCP bridge auto-start to support sse-remote mode
voarsh Mar 8, 2026
ca0c8b3
feat(watch,upload): add index journal for durable change tracking and…
voarsh Mar 9, 2026
c6fcf50
fix(core): improve pagination, upload reliability, and watch consistency
voarsh Mar 9, 2026
ecaf1c1
fix(code review): address critical and major issues from CodeRabbit
github-actions[bot] Mar 9, 2026
86f2212
fix(watch,consistency): improve error handling and retry logic
github-actions[bot] Mar 9, 2026
6bd58ea
fix(ingest,watch,upload): address CodeRabbit critical and major issues
github-actions[bot] Mar 9, 2026
99e9433
refactor(upload_service): extract duplicated collection resolution logic
github-actions[bot] Mar 9, 2026
37890c2
fix(upload,consistency): address CodeRabbit critical and major issues
github-actions[bot] Mar 9, 2026
37349d4
fix(watch,upload): restore internal path checks and async queued uplo…
voarsh Mar 9, 2026
984838d
fix(ingest,watch,upload): improve error handling and smart reindex fa…
voarsh Mar 9, 2026
b349a74
fix(ingest,upload,search): improve error handling and fix edge cases
voarsh Mar 9, 2026
ed627c7
fix(bridge): add retry logic for transient errors in MCP list operations
voarsh Mar 9, 2026
e161c14
ci(cosqa): add benchmark workflow and search matrix runner
voarsh Mar 9, 2026
714e5a4
Stabilize dense search, pseudo backfill, and graph consistency
voarsh Apr 30, 2026
e675858
Implements MCP session recovery for list operations
voarsh Apr 30, 2026
1ef926a
Simplify search tooling around repo_search
voarsh Apr 30, 2026
ef94f52
Preserve structured results for TOON output
voarsh Apr 30, 2026
497f04a
fix(upload): render admin templates with request-first API
voarsh May 3, 2026
0bf4719
Bundles Python dependencies and simplifies interpreter detection
voarsh May 30, 2026
af74ac7
Pin Qdrant client and server versions
voarsh Jun 1, 2026
517200c
Add watcher-managed init maintenance
voarsh Jun 1, 2026
0e32197
Simplify script imports and run entrypoints as modules
voarsh Jun 1, 2026
95a5952
Stop upload and memory flows from creating default collections
voarsh Jun 2, 2026
9c37722
Run git history ingest as a package module
voarsh Jun 2, 2026
e7c3f3a
Fix journal discovery for split metadata roots
voarsh Jun 2, 2026
a3355c9
Add journal drain visibility and admin clear action
voarsh Jun 2, 2026
f8495f9
Split watcher journal and filesystem sources
voarsh Jun 2, 2026
de36e5c
test: trim facade-heavy imports and default suite drag
voarsh Jun 2, 2026
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
210 changes: 182 additions & 28 deletions ctx-mcp-bridge/src/mcpServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { loadAnyAuthEntry, loadAuthEntry, readConfig, saveAuthEntry } from "./authConfig.js";
import { maybeRemapToolArgs, maybeRemapToolResult } from "./resultPathMapping.js";
import * as oauthHandler from "./oauthHandler.js";
Expand Down Expand Up @@ -58,14 +64,46 @@ function dedupeTools(tools) {
return out;
}

function dedupeResources(resources) {
const seen = new Set();
const out = [];
for (const resource of resources) {
const uri = resource && typeof resource.uri === "string" ? resource.uri : "";
if (!uri || seen.has(uri)) {
continue;
}
seen.add(uri);
out.push(resource);
}
return out;
}

function dedupeResourceTemplates(templates) {
const seen = new Set();
const out = [];
for (const template of templates) {
const uri =
template && template.resourceTemplate && typeof template.resourceTemplate.uriTemplate === "string"
? template.resourceTemplate.uriTemplate
: "";
if (!uri || seen.has(uri)) {
continue;
}
seen.add(uri);
out.push(template);
}
return out;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async function listMemoryTools(client) {
if (!client) {
return [];
}
try {
const timeoutMs = getBridgeListTimeoutMs();
const remote = await withTimeout(
client.listTools(),
5000,
timeoutMs,
"memory tools/list",
);
return Array.isArray(remote?.tools) ? remote.tools.slice() : [];
Expand All @@ -75,6 +113,42 @@ async function listMemoryTools(client) {
}
}

async function listResourcesSafe(client, label) {
if (!client) {
return [];
}
try {
const timeoutMs = getBridgeListTimeoutMs();
const remote = await withTimeout(
client.listResources(),
timeoutMs,
`${label} resources/list`,
);
return Array.isArray(remote?.resources) ? remote.resources.slice() : [];
} catch (err) {
debugLog(`[ctxce] Error calling ${label} resources/list: ` + String(err));
return [];
}
}

async function listResourceTemplatesSafe(client, label) {
if (!client) {
return [];
}
try {
const timeoutMs = getBridgeListTimeoutMs();
const remote = await withTimeout(
client.listResourceTemplates(),
timeoutMs,
`${label} resources/templates/list`,
);
return Array.isArray(remote?.resourceTemplates) ? remote.resourceTemplates.slice() : [];
} catch (err) {
debugLog(`[ctxce] Error calling ${label} resources/templates/list: ` + String(err));
return [];
}
}

function withTimeout(promise, ms, label) {
return new Promise((resolve, reject) => {
let settled = false;
Expand Down Expand Up @@ -125,6 +199,25 @@ function getBridgeToolTimeoutMs() {
}
}

function getBridgeListTimeoutMs() {
try {
// Keep list operations on a separate budget from tools/call.
// Some streamable-http clients (including Codex) probe tools/resources early,
// and a short timeout here can make the bridge appear unavailable.
const raw = process.env.CTXCE_LIST_TIMEOUT_MSEC;
if (!raw) {
return 60000;
}
const parsed = Number.parseInt(String(raw), 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
return 60000;
}
return parsed;
} catch {
return 60000;
}
}

function selectClientForTool(name, indexerClient, memoryClient) {
if (!name) {
return indexerClient;
Expand Down Expand Up @@ -651,6 +744,7 @@ async function createBridgeServer(options) {
{
capabilities: {
tools: {},
resources: {},
},
},
);
Expand All @@ -664,9 +758,10 @@ async function createBridgeServer(options) {
if (!indexerClient) {
throw new Error("Indexer MCP client not initialized");
}
const timeoutMs = getBridgeListTimeoutMs();
remote = await withTimeout(
indexerClient.listTools(),
10000,
timeoutMs,
"indexer tools/list",
);
} catch (err) {
Expand All @@ -693,6 +788,57 @@ async function createBridgeServer(options) {
return { tools };
});

server.setRequestHandler(ListResourcesRequestSchema, async () => {
// Proxy resource discovery/read-through so clients that use MCP resources
// (not only tools) can access upstream indexer/memory resources directly.
await initializeRemoteClients(false);
const indexerResources = await listResourcesSafe(indexerClient, "indexer");
const memoryResources = await listResourcesSafe(memoryClient, "memory");
const resources = dedupeResources([...indexerResources, ...memoryResources]);
debugLog(`[ctxce] resources/list: returning ${resources.length} resources`);
return { resources };
});

server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
await initializeRemoteClients(false);
const indexerTemplates = await listResourceTemplatesSafe(indexerClient, "indexer");
const memoryTemplates = await listResourceTemplatesSafe(memoryClient, "memory");
const resourceTemplates = dedupeResourceTemplates([...indexerTemplates, ...memoryTemplates]);
debugLog(`[ctxce] resources/templates/list: returning ${resourceTemplates.length} templates`);
return { resourceTemplates };
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
await initializeRemoteClients(false);
const params = request.params || {};
const timeoutMs = getBridgeToolTimeoutMs();
const uri =
params && typeof params.uri === "string" ? params.uri : "<missing-uri>";
debugLog(`[ctxce] resources/read: ${uri}`);

const tryRead = async (client, label) => {
if (!client) {
return null;
}
try {
return await client.readResource(params, { timeout: timeoutMs });
} catch (err) {
debugLog(`[ctxce] resources/read failed on ${label}: ` + String(err));
return null;
}
};

const indexerResult = await tryRead(indexerClient, "indexer");
if (indexerResult) {
return indexerResult;
}
const memoryResult = await tryRead(memoryClient, "memory");
if (memoryResult) {
return memoryResult;
}
throw new Error(`Resource ${uri} not available on any configured MCP server`);
});

// tools/call → proxied to indexer or memory server
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const params = request.params || {};
Expand Down Expand Up @@ -843,6 +989,13 @@ export async function runHttpMcpServer(options) {
typeof options.port === "number"
? options.port
: Number.parseInt(process.env.CTXCE_HTTP_PORT || "30810", 10) || 30810;
// TODO(auth): replace this boolean toggle with explicit auth modes (none|required).
// In required mode, enforce Bearer auth on /mcp with consistent 401 challenges and
// only advertise OAuth metadata/endpoints when authentication is mandatory.
// In local/dev mode, leaving OAuth discovery off avoids clients entering an
// unnecessary OAuth path for otherwise unauthenticated bridge usage.
const oauthEnabled = String(process.env.CTXCE_ENABLE_OAUTH || "").trim().toLowerCase();
const oauthEndpointsEnabled = oauthEnabled === "1" || oauthEnabled === "true" || oauthEnabled === "yes";

const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
Expand All @@ -865,34 +1018,36 @@ export async function runHttpMcpServer(options) {
// OAuth 2.0 Endpoints (RFC9728 Protected Resource Metadata + RFC7591)
// ================================================================

// OAuth metadata endpoint (RFC9728)
if (parsedUrl.pathname === "/.well-known/oauth-authorization-server") {
oauthHandler.handleOAuthMetadata(req, res, issuerUrl);
return;
}
if (oauthEndpointsEnabled) {
// OAuth metadata endpoint (RFC9728)
if (parsedUrl.pathname === "/.well-known/oauth-authorization-server") {
oauthHandler.handleOAuthMetadata(req, res, issuerUrl);
return;
}

// OAuth Dynamic Client Registration endpoint (RFC7591)
if (parsedUrl.pathname === "/oauth/register" && req.method === "POST") {
oauthHandler.handleOAuthRegister(req, res);
return;
}
// OAuth Dynamic Client Registration endpoint (RFC7591)
if (parsedUrl.pathname === "/oauth/register" && req.method === "POST") {
oauthHandler.handleOAuthRegister(req, res);
return;
}

// OAuth authorize endpoint
if (parsedUrl.pathname === "/oauth/authorize") {
oauthHandler.handleOAuthAuthorize(req, res, parsedUrl.searchParams);
return;
}
// OAuth authorize endpoint
if (parsedUrl.pathname === "/oauth/authorize") {
oauthHandler.handleOAuthAuthorize(req, res, parsedUrl.searchParams);
return;
}

// Store session endpoint (helper for login page)
if (parsedUrl.pathname === "/oauth/store-session" && req.method === "POST") {
oauthHandler.handleOAuthStoreSession(req, res);
return;
}
// Store session endpoint (helper for login page)
if (parsedUrl.pathname === "/oauth/store-session" && req.method === "POST") {
oauthHandler.handleOAuthStoreSession(req, res);
return;
}

// OAuth token endpoint
if (parsedUrl.pathname === "/oauth/token" && req.method === "POST") {
oauthHandler.handleOAuthToken(req, res);
return;
// OAuth token endpoint
if (parsedUrl.pathname === "/oauth/token" && req.method === "POST") {
oauthHandler.handleOAuthToken(req, res);
return;
}
}

// ================================================================
Expand Down Expand Up @@ -1058,4 +1213,3 @@ function detectRepoName(workspace, config) {
const leaf = workspace ? path.basename(workspace) : "";
return leaf && SLUGGED_REPO_RE.test(leaf) ? leaf : null;
}

43 changes: 39 additions & 4 deletions scripts/collection_admin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import logging
import os
import json
import re
import shutil
import time
from pathlib import Path
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional, List

logger = logging.getLogger(__name__)

from scripts.auth_backend import mark_collection_deleted

try:
Expand Down Expand Up @@ -193,6 +196,7 @@ def delete_collection_everywhere(
out: Dict[str, Any] = {
"collection": name,
"qdrant_deleted": False,
"qdrant_graph_deleted": False,
"registry_marked_deleted": False,
"deleted_state_files": 0,
"deleted_managed_workspaces": 0,
Expand All @@ -209,6 +213,14 @@ def delete_collection_everywhere(
out["qdrant_deleted"] = True
except Exception:
out["qdrant_deleted"] = False
# Best-effort: also delete companion graph edges collection when present.
# This branch stores file-level edges in `<collection>_graph`.
if not name.endswith("_graph"):
try:
cli.delete_collection(collection_name=f"{name}_graph")
out["qdrant_graph_deleted"] = True
except Exception:
out["qdrant_graph_deleted"] = False
except Exception:
out["qdrant_deleted"] = False

Expand Down Expand Up @@ -359,8 +371,10 @@ def _manual_copy_points() -> None:
vectors_config = None
sparse_vectors_config = None

# Support vector-less collections (e.g. payload-only graph edge collections).
if vectors_config is None:
raise RuntimeError(f"Cannot determine vectors config for source collection {src}")
vectors_config = {}
vectorless = isinstance(vectors_config, dict) and not vectors_config

try:
cli.create_collection(
Expand Down Expand Up @@ -401,7 +415,7 @@ def _manual_copy_points() -> None:
limit=batch_limit,
offset=offset,
with_payload=True,
with_vectors=True,
with_vectors=(not vectorless),
)
except Exception as exc:
raise RuntimeError(f"Failed to scroll points from {src}: {exc}") from exc
Expand All @@ -414,7 +428,9 @@ def _manual_copy_points() -> None:
point_id = getattr(record, "id", None)
payload = getattr(record, "payload", None)
vector = None
if hasattr(record, "vector") and getattr(record, "vector") is not None:
if vectorless:
vector = {}
elif hasattr(record, "vector") and getattr(record, "vector") is not None:
vector = getattr(record, "vector")
elif hasattr(record, "vectors") and getattr(record, "vectors") is not None:
vector = getattr(record, "vectors")
Expand Down Expand Up @@ -477,4 +493,23 @@ def _count_points(name: str) -> Optional[int]:
# The manual path guarantees the destination gets the exact same points/payloads/vectors.
_manual_copy_points()

# Best-effort: copy the companion graph collection when copying a base collection.
# Graph edges are derived data and can be rebuilt, but copying avoids a cold-start window
# during staging cutovers where the clone has no graph.
if not src.endswith("_graph") and not dest.endswith("_graph"):
try:
copy_collection_qdrant(
source=f"{src}_graph",
target=f"{dest}_graph",
qdrant_url=base_url,
overwrite=overwrite,
)
except Exception as exc:
logger.debug(
"Best-effort graph collection copy %s_graph -> %s_graph failed: %s",
src,
dest,
exc,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dest
Loading