@contextstream/mcp-server
Advanced tools
+169
-11
| #!/usr/bin/env node | ||
| // src/hooks/on-read.ts | ||
| import * as fs2 from "node:fs"; | ||
| import * as path2 from "node:path"; | ||
| import { homedir as homedir2 } from "node:os"; | ||
| // src/hot-paths.ts | ||
| import * as fs from "node:fs"; | ||
| import * as path from "node:path"; | ||
| import { homedir } from "node:os"; | ||
| var STORE_VERSION = 1; | ||
| var STORE_DIR = path.join(homedir(), ".contextstream"); | ||
| var STORE_FILE = path.join(STORE_DIR, "hot-paths.json"); | ||
| var MAX_PATHS_PER_SCOPE = 400; | ||
| var HALF_LIFE_MS = 3 * 24 * 60 * 60 * 1e3; | ||
| function clamp(value, min, max) { | ||
| return Math.max(min, Math.min(max, value)); | ||
| } | ||
| function normalizePathKey(input) { | ||
| return input.replace(/\\/g, "/").trim(); | ||
| } | ||
| function toScopeKey(input) { | ||
| const workspace = input.workspace_id || "none"; | ||
| const project = input.project_id || "none"; | ||
| return `${workspace}:${project}`; | ||
| } | ||
| function signalWeight(signal) { | ||
| switch (signal) { | ||
| case "search_result": | ||
| return 1.8; | ||
| case "activity_edit": | ||
| return 1.4; | ||
| case "activity_focus": | ||
| return 1.2; | ||
| case "activity_read": | ||
| default: | ||
| return 1; | ||
| } | ||
| } | ||
| function looksBroadQuery(query) { | ||
| const q = query.trim().toLowerCase(); | ||
| if (!q) return true; | ||
| if (q.length <= 2) return true; | ||
| if (/^\*+$/.test(q) || q === "all files" || q === "everything") return true; | ||
| const tokens = q.split(/\s+/).filter(Boolean); | ||
| return tokens.length > 12; | ||
| } | ||
| var HotPathStore = class { | ||
| constructor() { | ||
| this.data = { version: STORE_VERSION, scopes: {} }; | ||
| this.load(); | ||
| } | ||
| recordPaths(scope, paths, signal) { | ||
| if (paths.length === 0) return; | ||
| const now = Date.now(); | ||
| const scopeKey = toScopeKey(scope); | ||
| const profile = this.ensureScope(scopeKey); | ||
| const weight = signalWeight(signal); | ||
| for (const raw of paths) { | ||
| const pathKey = normalizePathKey(raw); | ||
| if (!pathKey) continue; | ||
| const current = profile.paths[pathKey] || { score: 0, last_seen: now, hits: 0 }; | ||
| const decayed = this.decayedScore(current.score, current.last_seen, now); | ||
| profile.paths[pathKey] = { | ||
| score: decayed + weight, | ||
| last_seen: now, | ||
| hits: current.hits + 1 | ||
| }; | ||
| } | ||
| profile.updated_at = now; | ||
| this.pruneScope(profile); | ||
| this.persist(); | ||
| } | ||
| buildHint(input) { | ||
| const scopeKey = toScopeKey(input); | ||
| const profile = this.data.scopes[scopeKey]; | ||
| if (!profile) return void 0; | ||
| const now = Date.now(); | ||
| const baseEntries = Object.entries(profile.paths).map(([filePath, entry]) => ({ | ||
| path: filePath, | ||
| score: this.decayedScore(entry.score, entry.last_seen, now) | ||
| })).filter((entry) => entry.score > 0.05).sort((a, b) => b.score - a.score); | ||
| const active = (input.active_paths || []).map(normalizePathKey).filter(Boolean); | ||
| const merged = /* @__PURE__ */ new Map(); | ||
| for (const entry of baseEntries.slice(0, Math.max(12, input.limit || 8))) { | ||
| merged.set(entry.path, { | ||
| path: entry.path, | ||
| score: Number(entry.score.toFixed(4)), | ||
| source: "history" | ||
| }); | ||
| } | ||
| for (const activePath of active) { | ||
| const existing = merged.get(activePath); | ||
| if (existing) { | ||
| existing.score = Number((existing.score + 0.9).toFixed(4)); | ||
| } else { | ||
| merged.set(activePath, { path: activePath, score: 0.9, source: "active" }); | ||
| } | ||
| } | ||
| const limit = clamp(input.limit ?? 8, 1, 12); | ||
| const entries = [...merged.values()].sort((a, b) => b.score - a.score).slice(0, limit); | ||
| if (entries.length === 0) return void 0; | ||
| const scoreSum = entries.reduce((sum, item) => sum + item.score, 0); | ||
| const normalized = clamp(scoreSum / (limit * 2.5), 0, 1); | ||
| const confidencePenalty = looksBroadQuery(input.query) ? 0.55 : 1; | ||
| const confidence = Number((normalized * confidencePenalty).toFixed(3)); | ||
| return { | ||
| entries, | ||
| confidence, | ||
| generated_at: new Date(now).toISOString(), | ||
| profile_version: STORE_VERSION | ||
| }; | ||
| } | ||
| decayedScore(score, lastSeenMs, nowMs) { | ||
| const elapsed = Math.max(0, nowMs - lastSeenMs); | ||
| const decay = Math.pow(0.5, elapsed / HALF_LIFE_MS); | ||
| return score * decay; | ||
| } | ||
| ensureScope(scopeKey) { | ||
| if (!this.data.scopes[scopeKey]) { | ||
| this.data.scopes[scopeKey] = { paths: {}, updated_at: Date.now() }; | ||
| } | ||
| return this.data.scopes[scopeKey]; | ||
| } | ||
| pruneScope(profile) { | ||
| const entries = Object.entries(profile.paths); | ||
| if (entries.length <= MAX_PATHS_PER_SCOPE) return; | ||
| entries.sort((a, b) => b[1].score - a[1].score).slice(MAX_PATHS_PER_SCOPE).forEach(([key]) => delete profile.paths[key]); | ||
| } | ||
| load() { | ||
| try { | ||
| if (!fs.existsSync(STORE_FILE)) return; | ||
| const parsed = JSON.parse(fs.readFileSync(STORE_FILE, "utf-8")); | ||
| if (parsed?.version !== STORE_VERSION || !parsed.scopes) return; | ||
| this.data = parsed; | ||
| } catch { | ||
| } | ||
| } | ||
| persist() { | ||
| try { | ||
| fs.mkdirSync(STORE_DIR, { recursive: true }); | ||
| fs.writeFileSync(STORE_FILE, JSON.stringify(this.data)); | ||
| } catch { | ||
| } | ||
| } | ||
| }; | ||
| var globalHotPathStore = new HotPathStore(); | ||
| // src/hooks/on-read.ts | ||
| var ENABLED = process.env.CONTEXTSTREAM_READ_HOOK_ENABLED !== "false"; | ||
@@ -14,9 +158,9 @@ var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io"; | ||
| function loadConfigFromMcpJson(cwd) { | ||
| let searchDir = path.resolve(cwd); | ||
| let searchDir = path2.resolve(cwd); | ||
| for (let i = 0; i < 5; i++) { | ||
| if (!API_KEY) { | ||
| const mcpPath = path.join(searchDir, ".mcp.json"); | ||
| if (fs.existsSync(mcpPath)) { | ||
| const mcpPath = path2.join(searchDir, ".mcp.json"); | ||
| if (fs2.existsSync(mcpPath)) { | ||
| try { | ||
| const content = fs.readFileSync(mcpPath, "utf-8"); | ||
| const content = fs2.readFileSync(mcpPath, "utf-8"); | ||
| const config = JSON.parse(content); | ||
@@ -35,6 +179,6 @@ const csEnv = config.mcpServers?.contextstream?.env; | ||
| if (!WORKSPACE_ID) { | ||
| const csConfigPath = path.join(searchDir, ".contextstream", "config.json"); | ||
| if (fs.existsSync(csConfigPath)) { | ||
| const csConfigPath = path2.join(searchDir, ".contextstream", "config.json"); | ||
| if (fs2.existsSync(csConfigPath)) { | ||
| try { | ||
| const content = fs.readFileSync(csConfigPath, "utf-8"); | ||
| const content = fs2.readFileSync(csConfigPath, "utf-8"); | ||
| const csConfig = JSON.parse(content); | ||
@@ -48,3 +192,3 @@ if (csConfig.workspace_id) { | ||
| } | ||
| const parentDir = path.dirname(searchDir); | ||
| const parentDir = path2.dirname(searchDir); | ||
| if (parentDir === searchDir) break; | ||
@@ -54,6 +198,6 @@ searchDir = parentDir; | ||
| if (!API_KEY) { | ||
| const homeMcpPath = path.join(homedir(), ".mcp.json"); | ||
| if (fs.existsSync(homeMcpPath)) { | ||
| const homeMcpPath = path2.join(homedir2(), ".mcp.json"); | ||
| if (fs2.existsSync(homeMcpPath)) { | ||
| try { | ||
| const content = fs.readFileSync(homeMcpPath, "utf-8"); | ||
| const content = fs2.readFileSync(homeMcpPath, "utf-8"); | ||
| const config = JSON.parse(content); | ||
@@ -143,2 +287,9 @@ const csEnv = config.mcpServers?.contextstream?.env; | ||
| resultSummary = `Read file: ${target}`; | ||
| if (target) { | ||
| globalHotPathStore.recordPaths( | ||
| { workspace_id: WORKSPACE_ID || void 0, project_id: void 0 }, | ||
| [target], | ||
| "activity_read" | ||
| ); | ||
| } | ||
| break; | ||
@@ -149,2 +300,9 @@ case "Glob": | ||
| resultSummary = `Found ${globFiles.length} files matching ${target}`; | ||
| if (globFiles.length > 0) { | ||
| globalHotPathStore.recordPaths( | ||
| { workspace_id: WORKSPACE_ID || void 0, project_id: void 0 }, | ||
| globFiles.slice(0, 30), | ||
| "activity_focus" | ||
| ); | ||
| } | ||
| break; | ||
@@ -151,0 +309,0 @@ case "Grep": |
@@ -70,2 +70,4 @@ #!/usr/bin/env node | ||
| last_state_change_at: void 0, | ||
| index_wait_started_at: void 0, | ||
| index_wait_until: void 0, | ||
| updated_at: nowIso() | ||
@@ -100,2 +102,4 @@ }; | ||
| target.entry.last_context_at = nowIso(); | ||
| target.entry.index_wait_started_at = void 0; | ||
| target.entry.index_wait_until = void 0; | ||
| target.entry.updated_at = nowIso(); | ||
@@ -152,2 +156,40 @@ writeState(state); | ||
| } | ||
| 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); | ||
| } | ||
@@ -160,2 +202,5 @@ // 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/**"]; | ||
@@ -186,2 +231,29 @@ 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) { | ||
@@ -448,8 +520,18 @@ if (!fs2.existsSync(INDEX_STATUS_FILE)) { | ||
| } | ||
| const { isIndexed } = isProjectIndexed(cwd); | ||
| fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] isIndexed=${isIndexed} | ||
| const { isIndexed, isStale } = isProjectIndexed(cwd); | ||
| fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] isIndexed=${isIndexed}, isStale=${isStale} | ||
| `); | ||
| if (!isIndexed) { | ||
| fs2.appendFileSync(DEBUG_FILE, `[PreToolUse] Project not indexed, allowing | ||
| `); | ||
| 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); | ||
| } | ||
| allowTool(editorFormat, cwd, recordStateChange); | ||
@@ -456,0 +538,0 @@ } |
@@ -238,2 +238,4 @@ #!/usr/bin/env node | ||
| last_state_change_at: void 0, | ||
| index_wait_started_at: void 0, | ||
| index_wait_until: void 0, | ||
| updated_at: nowIso() | ||
@@ -302,9 +304,8 @@ }; | ||
| \u2705 IF indexed & fresh: Use mcp__contextstream__search(mode="auto", query="...") BEFORE Glob/Grep/Read/Explore/Task/EnterPlanMode | ||
| \u2705 IF NOT indexed OR stale: Use local tools (Glob/Grep/Read) directly | ||
| \u2705 IF search returns 0 results: Fallback to local tools (Glob/Grep/Read) | ||
| \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) | ||
| 3. WHEN LOCAL TOOLS (Glob/Grep/Read) ARE OK: | ||
| \u2705 Project is NOT indexed (index_status.indexed=false) | ||
| \u2705 Index is stale/outdated (>7 days old) | ||
| \u2705 ContextStream search returns 0 results or errors | ||
| \u2705 Stale/not-indexed grace window has elapsed (~20s default, configurable) | ||
| \u2705 ContextStream search still returns 0 results or errors after retry | ||
| \u2705 User explicitly requests local tools | ||
@@ -732,12 +733,11 @@ | ||
| **IF indexed=false OR last_indexed_at is stale (>7 days):** | ||
| \u2192 Use local tools (Glob/Grep/Read) directly | ||
| \u2192 OR run \`project(action="index")\` first, then search | ||
| \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 | ||
| **IF search returns 0 results or errors:** | ||
| **IF search still returns 0 results or errors after retry/window:** | ||
| \u2192 Fallback to local tools (Glob/Grep/Read) | ||
| ### \u2705 When Local Tools (Glob/Grep/Read) Are OK: | ||
| - Project is NOT indexed | ||
| - Index is stale/outdated (>7 days) | ||
| - ContextStream search returns 0 results | ||
| - Stale/not-indexed grace window has elapsed (~20s default, configurable) | ||
| - ContextStream search still returns 0 results after retry | ||
| - ContextStream returns errors | ||
@@ -749,3 +749,3 @@ - User explicitly requests local tools | ||
| 2. Check \`project(action="index_status")\` before searching | ||
| 3. If not indexed: use local tools OR wait for indexing | ||
| 3. If not indexed: wait for background refresh (~20s), retry search, then use local tools only after the grace window | ||
@@ -752,0 +752,0 @@ ### After File Changes (Edit/Write/Create): |
+3
-3
| { | ||
| "name": "@contextstream/mcp-server", | ||
| "mcpName": "io.github.contextstreamio/mcp-server", | ||
| "version": "0.4.68", | ||
| "version": "0.4.71", | ||
| "description": "ContextStream MCP server - v0.4.x with consolidated domain tools (~11 tools, ~75% token reduction). Code context, memory, search, and AI tools.", | ||
@@ -37,3 +37,3 @@ "type": "module", | ||
| "dependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.25.1", | ||
| "@modelcontextprotocol/sdk": ">=1.25.1 <1.28.0", | ||
| "ignore": "^7.0.5", | ||
@@ -76,4 +76,4 @@ "zod": "^3.23.8" | ||
| "overrides": { | ||
| "qs": "6.14.1" | ||
| "qs": "6.14.2" | ||
| } | ||
| } |
+86
-6
@@ -127,2 +127,14 @@ <p align="center"> | ||
| ## Global Fallback Workspace (Unmapped Folders) | ||
| ContextStream now supports a catch-all mode for random folders (for example `~` or ad-hoc dirs) that are not associated with a project/workspace yet. | ||
| - `init(...)` resolves normal folder mappings first (`.contextstream/config.json`, parent/global mappings). | ||
| - If no mapping exists, it uses a single hidden global fallback workspace (`.contextstream-global`) in workspace-only mode. | ||
| - Context/memory/session tools continue to work without hard setup errors. | ||
| - Project-bound actions (for example `project(action="ingest_local")`) return guided remediation to create/select a project instead of failing with a raw `project_id required` error. | ||
| - As soon as you enter a mapped project folder, that real workspace/project is prioritized and replaces fallback scope. | ||
| --- | ||
| ## Manual Configuration | ||
@@ -209,4 +221,23 @@ | ||
| For GitHub Copilot in VS Code, use project-level MCP at `.vscode/mcp.json`. | ||
| For GitHub Copilot in VS Code, the easiest path is the hosted remote MCP with built-in OAuth. Marketplace installs should write this remote server definition automatically. | ||
| **Hosted remote MCP (recommended)** | ||
| ```json | ||
| { | ||
| "servers": { | ||
| "contextstream": { | ||
| "type": "http", | ||
| "url": "https://mcp.contextstream.io/mcp?default_context_mode=fast" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| On first use, VS Code should prompt you to authorize ContextStream in the browser and then complete setup without an API key in the config file. | ||
| `npx @contextstream/mcp-server@latest setup` now defaults VS Code/Copilot to this hosted remote when you are using the production ContextStream cloud. To force a local runtime instead, run setup with `CONTEXTSTREAM_VSCODE_MCP_MODE=local`. | ||
| For self-hosted or non-default API deployments, local runtime remains the default: | ||
| **Rust MCP (recommended)** | ||
@@ -223,3 +254,8 @@ | ||
| "CONTEXTSTREAM_API_URL": "https://api.contextstream.io", | ||
| "CONTEXTSTREAM_API_KEY": "your_key" | ||
| "CONTEXTSTREAM_API_KEY": "your_key", | ||
| "CONTEXTSTREAM_TOOLSET": "complete", | ||
| "CONTEXTSTREAM_TRANSCRIPTS_ENABLED": "true", | ||
| "CONTEXTSTREAM_HOOK_TRANSCRIPTS_ENABLED": "true", | ||
| "CONTEXTSTREAM_SEARCH_LIMIT": "15", | ||
| "CONTEXTSTREAM_SEARCH_MAX_CHARS": "2400" | ||
| } | ||
@@ -242,3 +278,8 @@ } | ||
| "CONTEXTSTREAM_API_URL": "https://api.contextstream.io", | ||
| "CONTEXTSTREAM_API_KEY": "your_key" | ||
| "CONTEXTSTREAM_API_KEY": "your_key", | ||
| "CONTEXTSTREAM_TOOLSET": "complete", | ||
| "CONTEXTSTREAM_TRANSCRIPTS_ENABLED": "true", | ||
| "CONTEXTSTREAM_HOOK_TRANSCRIPTS_ENABLED": "true", | ||
| "CONTEXTSTREAM_SEARCH_LIMIT": "15", | ||
| "CONTEXTSTREAM_SEARCH_MAX_CHARS": "2400" | ||
| } | ||
@@ -273,3 +314,8 @@ } | ||
| "CONTEXTSTREAM_API_URL": "https://api.contextstream.io", | ||
| "CONTEXTSTREAM_API_KEY": "your_key" | ||
| "CONTEXTSTREAM_API_KEY": "your_key", | ||
| "CONTEXTSTREAM_TOOLSET": "complete", | ||
| "CONTEXTSTREAM_TRANSCRIPTS_ENABLED": "true", | ||
| "CONTEXTSTREAM_HOOK_TRANSCRIPTS_ENABLED": "true", | ||
| "CONTEXTSTREAM_SEARCH_LIMIT": "15", | ||
| "CONTEXTSTREAM_SEARCH_MAX_CHARS": "2400" | ||
| } | ||
@@ -291,3 +337,8 @@ } | ||
| "CONTEXTSTREAM_API_URL": "https://api.contextstream.io", | ||
| "CONTEXTSTREAM_API_KEY": "your_key" | ||
| "CONTEXTSTREAM_API_KEY": "your_key", | ||
| "CONTEXTSTREAM_TOOLSET": "complete", | ||
| "CONTEXTSTREAM_TRANSCRIPTS_ENABLED": "true", | ||
| "CONTEXTSTREAM_HOOK_TRANSCRIPTS_ENABLED": "true", | ||
| "CONTEXTSTREAM_SEARCH_LIMIT": "15", | ||
| "CONTEXTSTREAM_SEARCH_MAX_CHARS": "2400" | ||
| } | ||
@@ -312,2 +363,4 @@ } | ||
| - Node install: use `npx --prefer-online -y @contextstream/mcp-server@latest` as the command. | ||
| - Force local VS Code/Copilot setup with `CONTEXTSTREAM_VSCODE_MCP_MODE=local`. | ||
| - Force hosted remote VS Code/Copilot setup with `CONTEXTSTREAM_VSCODE_MCP_MODE=remote`. | ||
| - Use `mcpServers` in Copilot CLI config and `servers` in VS Code config. | ||
@@ -322,6 +375,33 @@ | ||
| ## Known Limitations | ||
| ### HTTP transport OAuth and vscode.dev dependency | ||
| The hosted HTTP MCP transport (`https://mcp.contextstream.io/mcp`) uses OAuth authentication that routes through `vscode.dev` for the redirect flow. This can fail in environments where `vscode.dev` is blocked (corporate networks, regional restrictions, CDN-level blocks). | ||
| **Workaround:** Use the stdio transport (Rust binary or Node.js) with API key authentication instead: | ||
| ```json | ||
| { | ||
| "contextstream": { | ||
| "type": "stdio", | ||
| "command": "npx", | ||
| "args": ["-y", "@contextstream/mcp-server@latest"], | ||
| "env": { | ||
| "CONTEXTSTREAM_API_KEY": "your-api-key" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ### SDK version compatibility | ||
| `@modelcontextprotocol/sdk` versions 1.28.0 and above introduce breaking changes. The `package.json` pins the SDK to `>=1.25.1 <1.28.0` to prevent incompatible resolutions. If you experience Zod schema errors on startup, ensure your SDK version is below 1.28.0. | ||
| ## Marketplace Note | ||
| Marketplace npm installs can pin Node MCP versions and do not run external bootstrap scripts (`curl ... | bash` / `irm ... | iex`). Use the Rust install command directly when you want the Rust runtime. | ||
| The MCP marketplace entry now targets the hosted remote MCP at `https://mcp.contextstream.io/mcp?default_context_mode=fast` so VS Code can use the native OAuth flow instead of writing a local npm-based stdio config. | ||
| Use the Rust or Node local runtime configs above only when you explicitly want local execution, custom/self-hosted endpoints, or editor environments that do not support the hosted remote flow. | ||
| --- | ||
@@ -328,0 +408,0 @@ |
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.
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.
AI-detected potential malware
Supply chain riskAI has identified this package as malware. This is a strong signal that the package may be malicious.
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.
2041043
2.92%54430
2.17%413
24.02%1
-50%228
1.79%+ Added
+ Added
- Removed
- Removed