@contextstream/mcp-server
Advanced tools
| process.env.CONTEXTSTREAM_API_KEY ||= "cs_test_dummy"; | ||
| process.env.CONTEXTSTREAM_API_URL ||= "https://api.contextstream.io"; | ||
| process.env.CONTEXTSTREAM_LOG_LEVEL = "quiet"; | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| const { loadConfig } = await import("../src/config.js"); | ||
| const { ContextStreamClient } = await import("../src/client.js"); | ||
| const { registerTools } = await import("../src/tools.js"); | ||
| const { SessionManager } = await import("../src/session-manager.js"); | ||
| const config = loadConfig(); | ||
| const client = new ContextStreamClient(config); | ||
| const server = new McpServer({ name: "smoke", version: "0.0.0" }); | ||
| const sessionManager = new SessionManager(server, client); | ||
| registerTools(server, client, sessionManager, { toolSurfaceProfile: config.toolSurfaceProfile }); | ||
| const registered = (server as any)._registeredTools; | ||
| const names = registered ? Object.keys(registered).sort() : []; | ||
| console.log(`TOOL_COUNT=${names.length}`); | ||
| console.log(names.join("\n")); | ||
| const mustHave = [ | ||
| "qa", | ||
| "capture_plan", | ||
| "session_capture", | ||
| "session_capture_lesson", | ||
| "session_remember", | ||
| "memory_create_doc", | ||
| "memory_update_doc", | ||
| "memory_delete_doc", | ||
| "memory_create_task", | ||
| "memory_update_task", | ||
| "memory_create_todo", | ||
| "memory_complete_todo", | ||
| "memory_create_event", | ||
| ]; | ||
| const mustNotHave = ["ram", "mem", "chart", "async_job", "atlas_chart", "atlas_job"]; | ||
| const missing = mustHave.filter((n) => !names.includes(n)); | ||
| const forbidden = mustNotHave.filter((n) => names.includes(n)); | ||
| if (missing.length || forbidden.length) { | ||
| console.error(`SMOKE_FAIL missing=[${missing.join(",")}] forbidden=[${forbidden.join(",")}]`); | ||
| process.exit(1); | ||
| } | ||
| console.log("SMOKE_PASS"); |
@@ -154,40 +154,2 @@ #!/usr/bin/env node | ||
| } | ||
| function startIndexWaitWindow(cwd, waitSeconds) { | ||
| if (!cwd.trim() || waitSeconds <= 0) return; | ||
| const state = readState(); | ||
| const target = getOrCreateEntry(state, cwd); | ||
| if (!target) return; | ||
| const now = Date.now(); | ||
| const existingUntil = target.entry.index_wait_until ? new Date(target.entry.index_wait_until).getTime() : NaN; | ||
| if (!Number.isNaN(existingUntil) && existingUntil > now) { | ||
| target.entry.updated_at = nowIso(); | ||
| writeState(state); | ||
| return; | ||
| } | ||
| target.entry.index_wait_started_at = new Date(now).toISOString(); | ||
| target.entry.index_wait_until = new Date(now + waitSeconds * 1e3).toISOString(); | ||
| target.entry.updated_at = nowIso(); | ||
| writeState(state); | ||
| } | ||
| function clearIndexWaitWindow(cwd) { | ||
| if (!cwd.trim()) return; | ||
| const state = readState(); | ||
| const target = getOrCreateEntry(state, cwd); | ||
| if (!target) return; | ||
| target.entry.index_wait_started_at = void 0; | ||
| target.entry.index_wait_until = void 0; | ||
| target.entry.updated_at = nowIso(); | ||
| writeState(state); | ||
| } | ||
| function indexWaitRemainingSeconds(cwd) { | ||
| if (!cwd.trim()) return null; | ||
| const state = readState(); | ||
| const target = getOrCreateEntry(state, cwd); | ||
| if (!target?.entry.index_wait_until) return null; | ||
| const until = new Date(target.entry.index_wait_until).getTime(); | ||
| if (Number.isNaN(until)) return null; | ||
| const remainingMs = until - Date.now(); | ||
| if (remainingMs <= 0) return null; | ||
| return Math.ceil(remainingMs / 1e3); | ||
| } | ||
@@ -200,5 +162,2 @@ // src/hooks/pre-tool-use.ts | ||
| var CONTEXT_FRESHNESS_SECONDS = 120; | ||
| var DEFAULT_INDEX_WAIT_SECONDS = 20; | ||
| var MIN_INDEX_WAIT_SECONDS = 15; | ||
| var MAX_INDEX_WAIT_SECONDS = 20; | ||
| var DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]; | ||
@@ -229,29 +188,2 @@ function isDiscoveryGlob(pattern) { | ||
| } | ||
| function configuredIndexWaitSeconds() { | ||
| const parsed = Number.parseInt(process.env.CONTEXTSTREAM_INDEX_WAIT_SECONDS ?? "", 10); | ||
| if (Number.isNaN(parsed)) return DEFAULT_INDEX_WAIT_SECONDS; | ||
| return Math.min(MAX_INDEX_WAIT_SECONDS, Math.max(MIN_INDEX_WAIT_SECONDS, parsed)); | ||
| } | ||
| function isLocalDiscoveryToolDuringIndexWait(tool, toolInput) { | ||
| if (tool === "Glob" || tool === "Explore" || tool === "SemanticSearch" || tool === "codebase_search") { | ||
| return true; | ||
| } | ||
| if (tool === "Task") { | ||
| const subagentTypeRaw = toolInput?.subagent_type || toolInput?.subagentType || ""; | ||
| return subagentTypeRaw.toLowerCase().includes("explore"); | ||
| } | ||
| if (tool === "Grep" || tool === "Search" || tool === "grep_search" || tool === "code_search") { | ||
| const filePath = toolInput?.path || ""; | ||
| return isDiscoveryGrep(filePath); | ||
| } | ||
| if (tool === "Read" || tool === "ReadFile" || tool === "read_file") { | ||
| const filePath = toolInput?.file_path || toolInput?.path || toolInput?.file || toolInput?.target_file || ""; | ||
| return isDiscoveryGrep(filePath); | ||
| } | ||
| if (tool === "list_files" || tool === "search_files" || tool === "search_files_content" || tool === "find_files" || tool === "find_by_name") { | ||
| const pattern = toolInput?.path || toolInput?.regex || toolInput?.pattern || toolInput?.query || ""; | ||
| return !pattern || isDiscoveryGlob(pattern) || isDiscoveryGrep(pattern); | ||
| } | ||
| return false; | ||
| } | ||
| function isProjectIndexed(cwd) { | ||
@@ -426,7 +358,9 @@ if (!fs2.existsSync(INDEX_STATUS_FILE)) { | ||
| function outputCursorBlock(reason) { | ||
| console.log(JSON.stringify({ decision: "deny", reason })); | ||
| console.log( | ||
| JSON.stringify({ permission: "deny", agent_message: reason, user_message: reason }) | ||
| ); | ||
| process.exit(0); | ||
| } | ||
| function outputCursorAllow() { | ||
| console.log(JSON.stringify({ decision: "allow" })); | ||
| console.log(JSON.stringify({ permission: "allow" })); | ||
| process.exit(0); | ||
@@ -454,2 +388,5 @@ } | ||
| function detectEditorFormat(input) { | ||
| if (process.argv.includes("--editor=cursor")) { | ||
| return "cursor"; | ||
| } | ||
| if (input.hookName !== void 0 || input.toolName !== void 0) { | ||
@@ -523,15 +460,3 @@ return "cline"; | ||
| `); | ||
| if (isIndexed && !isStale) { | ||
| clearIndexWaitWindow(cwd); | ||
| } else { | ||
| const waitSeconds = configuredIndexWaitSeconds(); | ||
| if (isLocalDiscoveryToolDuringIndexWait(tool, toolInput)) { | ||
| startIndexWaitWindow(cwd, waitSeconds); | ||
| const remaining = indexWaitRemainingSeconds(cwd); | ||
| if (remaining && remaining > 0) { | ||
| const msg = `Index refresh grace window is active (${remaining}s remaining). Keep ContextStream search-first flow: mcp__contextstream__search(mode="auto", query="..."). Do not use local discovery tools yet for stale/not-indexed projects. Retry after refresh; local fallback is allowed only after ~${waitSeconds}s if index is still unavailable.`; | ||
| blockWithMessage(editorFormat, msg); | ||
| } | ||
| allowTool(editorFormat, cwd, recordStateChange); | ||
| } | ||
| if (!isIndexed || isStale) { | ||
| allowTool(editorFormat, cwd, recordStateChange); | ||
@@ -538,0 +463,0 @@ } |
@@ -301,10 +301,9 @@ #!/usr/bin/env node | ||
| 2. FOR CODE SEARCH: Check index status, then search appropriately | ||
| \u26A0\uFE0F BEFORE searching: mcp__contextstream__project(action="index_status") | ||
| \u2705 IF indexed & fresh: Use mcp__contextstream__search(mode="auto", query="...") BEFORE Glob/Grep/Read/Explore/Task/EnterPlanMode | ||
| \u2705 IF NOT indexed OR stale: wait for background refresh up to ~20s, retry mcp__contextstream__search(mode="auto", query="..."), then allow local tools only after the grace window | ||
| \u2705 IF search still returns 0 results after retry/window: fallback to local tools (Glob/Grep/Read) | ||
| \u2705 Use mcp__contextstream__search(mode="auto", query="...") BEFORE Glob/Grep/Read/Explore/Task/EnterPlanMode | ||
| \u2705 Run search even while the index is still building \u2014 keyword hits return immediately, results fill in as you work | ||
| \u2705 IF search itself returns 0 results/errors: retry once, THEN fall back to local tools (Glob/Grep/Read) | ||
| 3. WHEN LOCAL TOOLS (Glob/Grep/Read) ARE OK: | ||
| \u2705 Stale/not-indexed grace window has elapsed (~20s default, configurable) | ||
| \u2705 ContextStream search still returns 0 results or errors after retry | ||
| \u2705 The file was just written this turn (known-new, not yet indexed) | ||
| \u2705 User explicitly requests local tools | ||
@@ -732,4 +731,4 @@ | ||
| **IF indexed=false OR last_indexed_at is stale (>7 days):** | ||
| \u2192 Wait up to ~20s for background refresh, retry \`search(mode="auto", query="...")\` | ||
| \u2192 After grace window: local tools are allowed if search still misses | ||
| \u2192 Run \`search(mode="auto", query="...")\` anyway \u2014 keyword hits return immediately while indexing builds | ||
| \u2192 Local tools only if search itself returns 0 results/errors on a retry | ||
@@ -736,0 +735,0 @@ **IF search still returns 0 results or errors after retry/window:** |
+3
-3
| { | ||
| "name": "@contextstream/mcp-server", | ||
| "mcpName": "io.github.contextstreamio/mcp-server", | ||
| "version": "0.4.74", | ||
| "description": "ContextStream MCP server - v0.4.x with consolidated domain tools (~11 tools, ~75% token reduction). Code context, memory, search, and AI tools.", | ||
| "version": "0.4.80", | ||
| "description": "ContextStream MCP server - v0.4.x with consolidated domain tools plus per-action write surfaces (~75% token reduction vs individual tools). Code context, memory, search, Q&A, and AI tools.", | ||
| "type": "module", | ||
@@ -52,3 +52,3 @@ "license": "MIT", | ||
| "typescript-eslint": "^8.52.0", | ||
| "vitest": "^4.0.16" | ||
| "vitest": "^4.1.9" | ||
| }, | ||
@@ -55,0 +55,0 @@ "engines": { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
2324757
11.74%30
3.45%59772
8.2%231
1.76%