diff --git a/AGENTS.md b/AGENTS.md index 073beb9..415c9fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,8 @@ cd native && cargo build --release && napi build --release --platform ``` src/ ├── index.ts # Plugin entry: exports tools + slash commands +├── mcp-server.ts # MCP server: wraps Indexer for Cursor/Claude Code/Windsurf +├── cli.ts # CLI entry point for MCP stdio transport ├── config/ # Config schema (Zod) + parsing ├── embeddings/ # Provider detection (auto/github/openai/google/ollama) ├── indexer/ # Core: Indexer class, delta tracking @@ -72,6 +74,7 @@ skill/ # OpenCode skill guidance | Add database operation | `native/src/db.rs` + expose in `lib.rs` | | Add slash command | `commands/` + register in `src/index.ts` config() | +| Add/modify MCP tool | `src/mcp-server.ts` (createMcpServer) | ## CODE MAP ### TypeScript Exports (`src/index.ts`) @@ -87,6 +90,16 @@ skill/ # OpenCode skill guidance | `index_metrics` | Tool | Get performance metrics (requires debug.enabled + debug.metrics) | | `index_logs` | Tool | Get debug logs (requires debug.enabled) | + +### MCP Server Exports (`src/mcp-server.ts`) +| Symbol | Type | Purpose | +|--------|------|---------| +| `createMcpServer` | fn | Creates MCP Server with 8 tools + 4 prompts, lazy Indexer init | + +### CLI Entry (`src/cli.ts`) +| Symbol | Type | Purpose | +|--------|------|---------| +| `main` | fn | Parses --project/--config args, starts stdio transport, handles shutdown | ### Rust NAPI Exports (`native/src/lib.rs`) | Symbol | Type | Purpose | |--------|------|---------| @@ -204,6 +217,7 @@ afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); }); | `commands.test.ts` | Slash command loader, frontmatter parsing | | `logger.test.ts` | Logger utility, metrics collection | +| `mcp-server.test.ts` | MCP server: tool/prompt registration, execution via InMemoryTransport | ### Benchmarks ```bash npx tsx benchmarks/run.ts # Performance testing for native operations diff --git a/README.md b/README.md index a13ed07..bb319af 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ > **Stop grepping for concepts. Start searching for meaning.** -**opencode-codebase-index** brings semantic understanding to your [OpenCode](https://opencode.ai) workflow. Instead of guessing function names or grepping for keywords, ask your codebase questions in plain English. +**opencode-codebase-index** brings semantic understanding to your [OpenCode](https://opencode.ai) workflow — and now to any MCP-compatible client like Cursor, Claude Code, and Windsurf. Instead of guessing function names or grepping for keywords, ask your codebase questions in plain English. ## 🚀 Why Use This? @@ -16,7 +16,8 @@ - ⚡ **Blazing Fast Indexing**: Powered by a Rust native module using `tree-sitter` and `usearch`. Incremental updates take milliseconds. - 🌿 **Branch-Aware**: Seamlessly handles git branch switches — reuses embeddings, filters stale results. - 🔒 **Privacy Focused**: Your vector index is stored locally in your project. -- 🔌 **Model Agnostic**: Works out-of-the-box with GitHub Copilot, OpenAI, Gemini, or local Ollama models. + 🔌 **Model Agnostic**: Works out-of-the-box with GitHub Copilot, OpenAI, Gemini, or local Ollama models. + 🌐 **MCP Server**: Use with Cursor, Claude Code, Windsurf, or any MCP-compatible client — index once, search from anywhere. ## ⚡ Quick Start @@ -39,6 +40,52 @@ Ask: > "Find the function that handles credit card validation errors" +## 🌐 MCP Server (Cursor, Claude Code, Windsurf, etc.) + +Use the same semantic search from any MCP-compatible client. Index once, search from anywhere. + +1. **Install dependencies** + ```bash + npm install opencode-codebase-index @modelcontextprotocol/sdk zod + ``` + +2. **Configure your MCP client** + + **Cursor** (`.cursor/mcp.json`): + ```json + { + "mcpServers": { + "codebase-index": { + "command": "npx", + "args": ["opencode-codebase-index-mcp", "--project", "/path/to/your/project"] + } + } + } + ``` + + **Claude Code** (`claude_desktop_config.json`): + ```json + { + "mcpServers": { + "codebase-index": { + "command": "npx", + "args": ["opencode-codebase-index-mcp", "--project", "/path/to/your/project"] + } + } + } + ``` + +3. **CLI options** + ```bash + npx opencode-codebase-index-mcp --project /path/to/repo # specify project root + npx opencode-codebase-index-mcp --config /path/to/config # custom config file + npx opencode-codebase-index-mcp # uses current directory + ``` + +The MCP server exposes all 8 tools (`codebase_search`, `codebase_peek`, `find_similar`, `index_codebase`, `index_status`, `index_health_check`, `index_metrics`, `index_logs`) and 4 prompts (`search`, `find`, `index`, `status`). + +The MCP dependencies (`@modelcontextprotocol/sdk`, `zod`) are optional peer dependencies — they're only needed if you use the MCP server. + ## 🔍 See It In Action **Scenario**: You're new to a codebase and need to fix a bug in the payment flow. @@ -494,6 +541,8 @@ CI will automatically run tests and type checking on your PR. ``` ├── src/ │ ├── index.ts # Plugin entry point +│ ├── mcp-server.ts # MCP server (Cursor, Claude Code, Windsurf) +│ ├── cli.ts # CLI entry for MCP stdio transport │ ├── config/ # Configuration schema │ ├── embeddings/ # Provider detection and API calls │ ├── indexer/ # Core indexing logic + inverted index diff --git a/package-lock.json b/package-lock.json index 684aa98..8b1e086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opencode-codebase-index", - "version": "0.3.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-codebase-index", - "version": "0.3.0", + "version": "0.4.1", "license": "MIT", "dependencies": { "chokidar": "^5.0.0", @@ -15,8 +15,12 @@ "p-retry": "^7.1.1", "tiktoken": "^1.0.15" }, + "bin": { + "opencode-codebase-index-mcp": "dist/cli.js" + }, "devDependencies": { "@eslint/js": "^9.39.2", + "@modelcontextprotocol/sdk": "^1.12.1", "@napi-rs/cli": "^3.5.1", "@opencode-ai/plugin": "^1.1.21", "@types/node": "^25.0.8", @@ -25,13 +29,27 @@ "tsup": "^8.1.0", "typescript": "^5.5.0", "typescript-eslint": "^8.53.0", - "vitest": "^4.0.17" + "vitest": "^4.0.17", + "zod": "^3.25.67" }, "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "@opencode-ai/plugin": "^1.0.0" + "@modelcontextprotocol/sdk": "^1.12.1", + "@opencode-ai/plugin": "^1.0.0", + "zod": "^3.0.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + }, + "@opencode-ai/plugin": { + "optional": true + }, + "zod": { + "optional": true + } } }, "node_modules/@babel/helper-string-parser": { @@ -724,6 +742,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1159,6 +1190,71 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/@napi-rs/cli": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-3.5.1.tgz", @@ -2161,7 +2257,6 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -2746,7 +2841,6 @@ "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2786,7 +2880,6 @@ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -3140,13 +3233,26 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3181,6 +3287,48 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -3260,6 +3408,31 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3287,6 +3460,16 @@ "esbuild": ">=0.18" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3297,6 +3480,37 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3443,6 +3657,68 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3483,6 +3759,38 @@ "dev": true, "license": "MIT" }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/emnapi": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/emnapi/-/emnapi-1.8.1.tgz", @@ -3505,6 +3813,36 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -3512,6 +3850,19 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-toolkit": { "version": "1.43.0", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz", @@ -3530,7 +3881,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -3566,6 +3916,13 @@ "@esbuild/win32-x64": "0.27.2" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3585,7 +3942,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3767,12 +4123,45 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3783,6 +4172,69 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fast-content-type-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", @@ -3821,19 +4273,58 @@ "dev": true, "license": "MIT" }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, "engines": { "node": ">=16.0.0" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3884,6 +4375,26 @@ "dev": true, "license": "ISC" }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3899,6 +4410,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-east-asian-width": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", @@ -3912,6 +4433,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -3925,6 +4485,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3935,6 +4508,42 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.1.tgz", + "integrity": "sha512-hi9afu8g0lfJVLolxElAZGANCTTl6bewIdsRNhaywfP9K8BPf++F2z6OLrYGIinUwpRKzbZHMhPwvc0ZEpAwGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3942,6 +4551,27 @@ "dev": true, "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -3995,6 +4625,33 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4030,6 +4687,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4076,6 +4740,16 @@ "node": ">=8" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -4120,6 +4794,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -4242,6 +4923,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4323,6 +5064,16 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4333,6 +5084,19 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -4344,6 +5108,29 @@ ], "license": "MIT" }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4450,6 +5237,16 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4470,6 +5267,17 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -4494,6 +5302,16 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -4526,7 +5344,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4589,6 +5406,20 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4599,6 +5430,48 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -4612,6 +5485,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4667,6 +5550,23 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4687,6 +5587,60 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4710,6 +5664,82 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -4757,6 +5787,16 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -4931,7 +5971,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4949,6 +5988,16 @@ "node": ">=14.0.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -5103,13 +6152,27 @@ "node": ">= 0.8.0" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5163,6 +6226,16 @@ "dev": true, "license": "ISC" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5173,13 +6246,22 @@ "punycode": "^2.1.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -5273,7 +6355,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5456,6 +6537,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -5468,6 +6556,26 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/package.json b/package.json index 9be436c..caa2704 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "require": "./dist/index.cjs" } }, + "bin": { + "opencode-codebase-index-mcp": "dist/cli.js" + }, "license": "MIT", "author": "Kenneth", "repository": { @@ -68,6 +71,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.2", + "@modelcontextprotocol/sdk": "^1.12.1", "@napi-rs/cli": "^3.5.1", "@opencode-ai/plugin": "^1.1.21", "@types/node": "^25.0.8", @@ -76,9 +80,24 @@ "tsup": "^8.1.0", "typescript": "^5.5.0", "typescript-eslint": "^8.53.0", - "vitest": "^4.0.17" + "vitest": "^4.0.17", + "zod": "^3.25.67" }, "peerDependencies": { - "@opencode-ai/plugin": "^1.0.0" + "@modelcontextprotocol/sdk": "^1.12.1", + "@opencode-ai/plugin": "^1.0.0", + "zod": "^3.0.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + }, + "@opencode-ai/plugin": { + "optional": true + }, + "zod": { + "optional": true + } } -} + +} \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..97b7e99 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,73 @@ +#!/usr/bin/env node +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { existsSync, readFileSync } from "fs"; +import * as path from "path"; +import * as os from "os"; + +import { parseConfig } from "./config/schema.js"; +import { createMcpServer } from "./mcp-server.js"; + +function loadJsonFile(filePath: string): unknown { + try { + if (existsSync(filePath)) { + const content = readFileSync(filePath, "utf-8"); + return JSON.parse(content); + } + } catch { /* ignore */ } + return null; +} + +function loadPluginConfig(projectRoot: string, configPath?: string): unknown { + if (configPath) { + const config = loadJsonFile(configPath); + if (config) return config; + } + + const projectConfig = loadJsonFile(path.join(projectRoot, ".opencode", "codebase-index.json")); + if (projectConfig) return projectConfig; + + const globalConfig = loadJsonFile(path.join(os.homedir(), ".config", "opencode", "codebase-index.json")); + if (globalConfig) return globalConfig; + + return {}; +} + +function parseArgs(argv: string[]): { project: string; config?: string } { + let project = process.cwd(); + let config: string | undefined; + + for (let i = 2; i < argv.length; i++) { + if (argv[i] === "--project" && argv[i + 1]) { + project = path.resolve(argv[++i]); + } else if (argv[i] === "--config" && argv[i + 1]) { + config = path.resolve(argv[++i]); + } + } + + return { project, config }; +} + +async function main(): Promise { + const args = parseArgs(process.argv); + const rawConfig = loadPluginConfig(args.project, args.config); + const config = parseConfig(rawConfig); + + const server = createMcpServer(args.project, config); + const transport = new StdioServerTransport(); + + await server.connect(transport); + + const shutdown = (): void => { + server.close().catch(() => {}); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + console.error(`Fatal: ${message}`); + process.exit(1); +}); diff --git a/src/mcp-server.ts b/src/mcp-server.ts new file mode 100644 index 0000000..41f9ed7 --- /dev/null +++ b/src/mcp-server.ts @@ -0,0 +1,424 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +import { Indexer, type IndexStats } from "./indexer/index.js"; +import type { ParsedCodebaseIndexConfig, LogLevel } from "./config/schema.js"; +import { formatCostEstimate } from "./utils/cost.js"; + +const MAX_CONTENT_LINES = 30; + +function truncateContent(content: string): string { + const lines = content.split("\n"); + if (lines.length <= MAX_CONTENT_LINES) return content; + return ( + lines.slice(0, MAX_CONTENT_LINES).join("\n") + + `\n// ... (${lines.length - MAX_CONTENT_LINES} more lines)` + ); +} + +function formatIndexStats(stats: IndexStats, verbose: boolean = false): string { + const lines: string[] = []; + + if (stats.indexedChunks === 0 && stats.removedChunks === 0) { + lines.push(`Indexed. ${stats.totalFiles} files processed, ${stats.existingChunks} code chunks already up to date.`); + } else if (stats.indexedChunks === 0) { + lines.push(`Indexed. ${stats.totalFiles} files, removed ${stats.removedChunks} stale chunks, ${stats.existingChunks} chunks remain.`); + } else { + let main = `Indexed. ${stats.totalFiles} files processed, ${stats.indexedChunks} new chunks embedded.`; + if (stats.existingChunks > 0) { + main += ` ${stats.existingChunks} unchanged chunks skipped.`; + } + lines.push(main); + + if (stats.removedChunks > 0) { + lines.push(`Removed ${stats.removedChunks} stale chunks.`); + } + + if (stats.failedChunks > 0) { + lines.push(`Failed: ${stats.failedChunks} chunks.`); + } + + lines.push(`Tokens: ${stats.tokensUsed.toLocaleString()}, Duration: ${(stats.durationMs / 1000).toFixed(1)}s`); + } + + if (verbose) { + if (stats.skippedFiles.length > 0) { + const tooLarge = stats.skippedFiles.filter(f => f.reason === "too_large"); + const excluded = stats.skippedFiles.filter(f => f.reason === "excluded"); + const gitignored = stats.skippedFiles.filter(f => f.reason === "gitignore"); + + lines.push(""); + lines.push(`Skipped files: ${stats.skippedFiles.length}`); + if (tooLarge.length > 0) { + lines.push(` Too large (${tooLarge.length}): ${tooLarge.slice(0, 5).map(f => f.path).join(", ")}${tooLarge.length > 5 ? "..." : ""}`); + } + if (excluded.length > 0) { + lines.push(` Excluded (${excluded.length}): ${excluded.slice(0, 5).map(f => f.path).join(", ")}${excluded.length > 5 ? "..." : ""}`); + } + if (gitignored.length > 0) { + lines.push(` Gitignored (${gitignored.length}): ${gitignored.slice(0, 5).map(f => f.path).join(", ")}${gitignored.length > 5 ? "..." : ""}`); + } + } + + if (stats.parseFailures.length > 0) { + lines.push(""); + lines.push(`Files with no extractable chunks (${stats.parseFailures.length}): ${stats.parseFailures.slice(0, 10).join(", ")}${stats.parseFailures.length > 10 ? "..." : ""}`); + } + } + + return lines.join("\n"); +} + +function formatStatus(status: { + indexed: boolean; + vectorCount: number; + provider: string; + model: string; + indexPath: string; + currentBranch: string; + baseBranch: string; +}): string { + if (!status.indexed) { + return "Codebase is not indexed. Run index_codebase to create an index."; + } + + const lines = [ + `Index status:`, + ` Indexed chunks: ${status.vectorCount.toLocaleString()}`, + ` Provider: ${status.provider}`, + ` Model: ${status.model}`, + ` Location: ${status.indexPath}`, + ]; + + if (status.currentBranch !== "default") { + lines.push(` Current branch: ${status.currentBranch}`); + lines.push(` Base branch: ${status.baseBranch}`); + } + + return lines.join("\n"); +} + +const CHUNK_TYPE_ENUM = [ + "function", "class", "method", "interface", "type", + "enum", "struct", "impl", "trait", "module", "other", +] as const; + +export function createMcpServer(projectRoot: string, config: ParsedCodebaseIndexConfig): McpServer { + const server = new McpServer({ + name: "opencode-codebase-index", + version: "0.4.1", + }); + + const indexer = new Indexer(projectRoot, config); + let initialized = false; + + async function ensureInitialized(): Promise { + if (!initialized) { + await indexer.initialize(); + initialized = true; + } + } + + // --- Tools --- + + server.tool( + "codebase_search", + "Search codebase by MEANING, not keywords. Returns full code content. For just finding WHERE code is (saves ~90% tokens), use codebase_peek instead.", + { + query: z.string().describe("Natural language description of what code you're looking for. Describe behavior, not syntax."), + limit: z.number().optional().default(5).describe("Maximum number of results to return"), + fileType: z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"), + directory: z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"), + chunkType: z.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"), + contextLines: z.number().optional().describe("Number of extra lines to include before/after each match (default: 0)"), + }, + async (args) => { + await ensureInitialized(); + const results = await indexer.search(args.query, args.limit ?? 5, { + fileType: args.fileType, + directory: args.directory, + chunkType: args.chunkType, + contextLines: args.contextLines, + }); + + if (results.length === 0) { + return { content: [{ type: "text", text: "No matching code found. Try a different query or run index_codebase first." }] }; + } + + const formatted = results.map((r, idx) => { + const header = r.name + ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` + : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`; + return `${header} (score: ${r.score.toFixed(2)})\n\`\`\`\n${truncateContent(r.content)}\n\`\`\``; + }); + + return { content: [{ type: "text", text: `Found ${results.length} results for "${args.query}":\n\n${formatted.join("\n\n")}` }] }; + }, + ); + + server.tool( + "codebase_peek", + "Quick lookup of code locations by meaning. Returns only metadata (file, line, name, type) WITHOUT code content. Saves ~90% tokens vs codebase_search.", + { + query: z.string().describe("Natural language description of what code you're looking for."), + limit: z.number().optional().default(10).describe("Maximum number of results to return"), + fileType: z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"), + directory: z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"), + chunkType: z.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"), + }, + async (args) => { + await ensureInitialized(); + const results = await indexer.search(args.query, args.limit ?? 10, { + fileType: args.fileType, + directory: args.directory, + chunkType: args.chunkType, + metadataOnly: true, + }); + + if (results.length === 0) { + return { content: [{ type: "text", text: "No matching code found. Try a different query or run index_codebase first." }] }; + } + + const formatted = results.map((r, idx) => { + const location = `${r.filePath}:${r.startLine}-${r.endLine}`; + const name = r.name ? `"${r.name}"` : "(anonymous)"; + return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})`; + }); + + return { content: [{ type: "text", text: `Found ${results.length} locations for "${args.query}":\n\n${formatted.join("\n")}\n\nUse Read tool to examine specific files.` }] }; + }, + ); + + server.tool( + "index_codebase", + "Index the codebase for semantic search. Creates vector embeddings of code chunks. Incremental - only re-indexes changed files. Run before first codebase_search.", + { + force: z.boolean().optional().default(false).describe("Force reindex even if already indexed"), + estimateOnly: z.boolean().optional().default(false).describe("Only show cost estimate without indexing"), + verbose: z.boolean().optional().default(false).describe("Show detailed info about skipped files and parsing failures"), + }, + async (args) => { + await ensureInitialized(); + + if (args.estimateOnly) { + const estimate = await indexer.estimateCost(); + return { content: [{ type: "text", text: formatCostEstimate(estimate) }] }; + } + + if (args.force) { + await indexer.clearIndex(); + } + + const stats = await indexer.index(); + return { content: [{ type: "text", text: formatIndexStats(stats, args.verbose ?? false) }] }; + }, + ); + + server.tool( + "index_status", + "Check the status of the codebase index. Shows whether the codebase is indexed, how many chunks are stored, and the embedding provider being used.", + {}, + async () => { + await ensureInitialized(); + const status = await indexer.getStatus(); + return { content: [{ type: "text", text: formatStatus(status) }] }; + }, + ); + + server.tool( + "index_health_check", + "Check index health and remove stale entries from deleted files. Run this to clean up the index after files have been deleted.", + {}, + async () => { + await ensureInitialized(); + const result = await indexer.healthCheck(); + + if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0) { + return { content: [{ type: "text", text: "Index is healthy. No stale entries found." }] }; + } + + const lines = [`Health check complete:`]; + + if (result.removed > 0) { + lines.push(` Removed stale entries: ${result.removed}`); + } + + if (result.gcOrphanEmbeddings > 0) { + lines.push(` Garbage collected orphan embeddings: ${result.gcOrphanEmbeddings}`); + } + + if (result.gcOrphanChunks > 0) { + lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`); + } + + if (result.filePaths.length > 0) { + lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`); + } + + return { content: [{ type: "text", text: lines.join("\n") }] }; + }, + ); + + server.tool( + "index_metrics", + "Get metrics and performance statistics for the codebase index. Requires debug.enabled=true and debug.metrics=true in config.", + {}, + async () => { + await ensureInitialized(); + const logger = indexer.getLogger(); + + if (!logger.isEnabled()) { + return { content: [{ type: "text", text: "Debug mode is disabled. Enable it in your config:\n\n```json\n{\n \"debug\": {\n \"enabled\": true,\n \"metrics\": true\n }\n}\n```" }] }; + } + + if (!logger.isMetricsEnabled()) { + return { content: [{ type: "text", text: "Metrics collection is disabled. Enable it in your config:\n\n```json\n{\n \"debug\": {\n \"enabled\": true,\n \"metrics\": true\n }\n}\n```" }] }; + } + + return { content: [{ type: "text", text: logger.formatMetrics() }] }; + }, + ); + + server.tool( + "index_logs", + "Get recent debug logs from the codebase indexer. Requires debug.enabled=true in config.", + { + limit: z.number().optional().default(20).describe("Maximum number of log entries to return"), + category: z.enum(["search", "embedding", "cache", "gc", "branch", "general"]).optional().describe("Filter by log category"), + level: z.enum(["error", "warn", "info", "debug"]).optional().describe("Filter by minimum log level"), + }, + async (args) => { + await ensureInitialized(); + const logger = indexer.getLogger(); + + if (!logger.isEnabled()) { + return { content: [{ type: "text", text: "Debug mode is disabled. Enable it in your config:\n\n```json\n{\n \"debug\": {\n \"enabled\": true\n }\n}\n```" }] }; + } + + let logs; + if (args.category) { + logs = logger.getLogsByCategory(args.category, args.limit); + } else if (args.level) { + logs = logger.getLogsByLevel(args.level as LogLevel, args.limit); + } else { + logs = logger.getLogs(args.limit); + } + + if (logs.length === 0) { + return { content: [{ type: "text", text: "No logs recorded yet. Logs are captured during indexing and search operations." }] }; + } + + const text = logs.map(l => { + const dataStr = l.data ? ` ${JSON.stringify(l.data)}` : ""; + return `[${l.timestamp}] [${l.level.toUpperCase()}] [${l.category}] ${l.message}${dataStr}`; + }).join("\n"); + + return { content: [{ type: "text", text }] }; + }, + ); + + server.tool( + "find_similar", + "Find code similar to a given snippet. Use for duplicate detection, pattern discovery, or refactoring prep.", + { + code: z.string().describe("The code snippet to find similar code for"), + limit: z.number().optional().default(10).describe("Maximum number of results to return"), + fileType: z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"), + directory: z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"), + chunkType: z.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"), + excludeFile: z.string().optional().describe("Exclude results from this file path"), + }, + async (args) => { + await ensureInitialized(); + const results = await indexer.findSimilar(args.code, args.limit ?? 10, { + fileType: args.fileType, + directory: args.directory, + chunkType: args.chunkType, + excludeFile: args.excludeFile, + }); + + if (results.length === 0) { + return { content: [{ type: "text", text: "No similar code found. Try a different snippet or run index_codebase first." }] }; + } + + const formatted = results.map((r, idx) => { + const header = r.name + ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` + : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`; + return `${header} (similarity: ${(r.score * 100).toFixed(1)}%)\n\`\`\`\n${truncateContent(r.content)}\n\`\`\``; + }); + + return { content: [{ type: "text", text: `Found ${results.length} similar code blocks:\n\n${formatted.join("\n\n")}` }] }; + }, + ); + + // --- Prompts --- + + server.prompt( + "search", + "Search codebase by meaning using semantic search", + { query: z.string().describe("What to search for in the codebase") }, + (args) => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: `Search the codebase for: "${args.query}"\n\nUse the codebase_search tool with this query. If you need just locations first, use codebase_peek instead to save tokens.`, + }, + }], + }), + ); + + server.prompt( + "find", + "Find code using hybrid approach (semantic + grep)", + { query: z.string().describe("What to find in the codebase") }, + (args) => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: `Find code related to: "${args.query}"\n\nUse a hybrid approach:\n1. First use codebase_peek to find semantic matches by meaning\n2. Then use grep for exact identifier matches\n3. Combine results for comprehensive coverage`, + }, + }], + }), + ); + + server.prompt( + "index", + "Index the codebase for semantic search", + { options: z.string().optional().describe("Options: 'force' to rebuild, 'estimate' to check costs") }, + (args) => { + const opts = args.options?.toLowerCase() ?? ""; + let instruction = "Use the index_codebase tool to index the codebase for semantic search."; + if (opts.includes("force")) { + instruction = "Use the index_codebase tool with force=true to rebuild the entire index from scratch."; + } else if (opts.includes("estimate")) { + instruction = "Use the index_codebase tool with estimateOnly=true to check the cost estimate before indexing."; + } + return { + messages: [{ + role: "user", + content: { type: "text", text: instruction }, + }], + }; + }, + ); + + server.prompt( + "status", + "Check if the codebase is indexed and ready", + {}, + () => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: "Use the index_status tool to check if the codebase index is ready and show its current state.", + }, + }], + }), + ); + + return server; +} diff --git a/tests/call-graph.test.ts b/tests/call-graph.test.ts new file mode 100644 index 0000000..6afe25d --- /dev/null +++ b/tests/call-graph.test.ts @@ -0,0 +1,129 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; + +describe("call-graph", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "call-graph-test-")); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + describe("call extraction", () => { + it.skip("should extract direct function calls", () => { + // TODO: Will test extractCalls() from native module + // Load tests/fixtures/call-graph/simple-calls.ts + // Parse and extract call_expression nodes + // Verify directCall, helper, compute are captured + }); + + it.skip("should extract method calls", () => { + // TODO: Will test obj.method() patterns + // Load tests/fixtures/call-graph/method-calls.ts + // Verify member_expression + call_expression captured + }); + + it.skip("should extract constructor calls", () => { + // TODO: Will test new Foo() patterns + // Load tests/fixtures/call-graph/constructors.ts + // Verify new_expression nodes captured + }); + + it.skip("should extract imports", () => { + // TODO: Will test import statement extraction + // Load tests/fixtures/call-graph/imports.ts + // Verify import_statement and named/default imports captured + }); + + it.skip("should handle nested calls", () => { + // TODO: Will test deeply nested call patterns + // Load tests/fixtures/call-graph/nested-calls.ts + // Verify all call levels captured correctly + }); + + it.skip("should handle edge cases", () => { + // TODO: Will test edge cases like optional chaining, dynamic imports + // Load tests/fixtures/call-graph/edge-cases.ts + // Verify obj?.method(), await import(), etc. + }); + }); + + describe("call graph storage", () => { + it.skip("should store symbols in database", () => { + // TODO: Will test db.upsertSymbolsBatch() + // Create Database instance + // Insert symbols with file_path, name, kind, line/col + // Verify symbols retrievable via query + }); + + it.skip("should store call edges", () => { + // TODO: Will test db.upsertCallEdgesBatch() + // Create symbols first + // Insert edges with from_symbol_id, target_name, is_resolved + // Verify edges retrievable + }); + + it.skip("should store branch relationships", () => { + // TODO: Will test addSymbolsToBranchBatch() + // Create branch and symbols + // Associate symbols with branch + // Verify branch filtering works + }); + }); + + describe("call resolution", () => { + it.skip("should resolve same-file calls", () => { + // TODO: Will test resolveSameFileEdges() + // Load tests/fixtures/call-graph/same-file-refs.ts + // Extract calls and symbols + // Run resolution logic + // Verify is_resolved=true and to_symbol_id set + }); + + it.skip("should leave cross-file calls unresolved", () => { + // TODO: Will test that imports remain is_resolved=false + // Load fixtures with imports + // Verify imported symbols have is_resolved=false + // Verify to_symbol_id is NULL + }); + + it.skip("should handle multiple targets with same name", () => { + // TODO: Will test disambiguation when same function name appears multiple times + // Create symbols with same name but different scopes + // Verify resolution picks correct target by scope/context + }); + }); + + describe("branch awareness", () => { + it.skip("should filter symbols by current branch", () => { + // TODO: Will test branch filtering + // Create symbols on branch A + // Create symbols on branch B + // Query with branch filter + // Verify only branch A symbols returned + }); + + it.skip("should filter call edges by branch", () => { + // TODO: Will test edge branch filtering + // Create edges on different branches + // Query with branch filter + // Verify only correct branch edges returned + }); + }); + + describe("integration", () => { + it.skip("should build complete call graph for simple project", () => { + // TODO: End-to-end test + // Parse multiple fixture files + // Extract all calls and symbols + // Store in database + // Resolve same-file calls + // Verify complete graph structure + }); + }); +}); diff --git a/tests/fixtures/call-graph/constructors.ts b/tests/fixtures/call-graph/constructors.ts new file mode 100644 index 0000000..5fae3cc --- /dev/null +++ b/tests/fixtures/call-graph/constructors.ts @@ -0,0 +1,53 @@ +// Constructor calls test fixture +// Tests new Constructor() patterns + +class SimpleClass { + constructor() {} +} + +class ClassWithArgs { + name: string; + value: number; + + constructor(name: string, value: number) { + this.name = name; + this.value = value; + } +} + +class NestedConstruction { + inner: SimpleClass; + + constructor() { + this.inner = new SimpleClass(); // Constructor call in constructor + } +} + +// Direct constructor calls +const obj1 = new SimpleClass(); +const obj2 = new ClassWithArgs("test", 42); +const obj3 = new NestedConstruction(); + +// Constructor with complex args +const obj4 = new ClassWithArgs( + getName(), + getValue() +); + +function getName() { + return "dynamic"; +} + +function getValue() { + return 100; +} + +// Generic constructor +class GenericBox { + value: T; + constructor(val: T) { + this.value = val; + } +} + +const box = new GenericBox(42); diff --git a/tests/fixtures/call-graph/edge-cases.ts b/tests/fixtures/call-graph/edge-cases.ts new file mode 100644 index 0000000..4129376 --- /dev/null +++ b/tests/fixtures/call-graph/edge-cases.ts @@ -0,0 +1,140 @@ +// Edge cases test fixture +// Tests optional chaining, dynamic imports, and other tricky patterns + +// Optional chaining +function optionalCalls(obj?: any) { + obj?.method(); // Optional method call + obj?.nested?.deep?.call(); // Multiple optional chains + const result = obj?.compute?.(5); // Optional call with args + return result; +} + +// Dynamic property access +function dynamicCalls(obj: any, methodName: string) { + obj[methodName](); // Dynamic method call + obj["staticName"](); // Bracket notation +} + +// IIFE (Immediately Invoked Function Expression) +(function immediate() { + console.log("IIFE"); +})(); + +// Arrow function immediate call +(() => { + setup(); +})(); + +function setup() {} + +// Conditional calls +function conditionalExecution(flag: boolean) { + if (flag) { + trueCase(); + } else { + falseCase(); + } + + flag ? whenTrue() : whenFalse(); // Ternary +} + +function trueCase() {} +function falseCase() {} +function whenTrue() {} +function whenFalse() {} + +// Try-catch with calls +function errorHandling() { + try { + riskyOperation(); + } catch (error) { + handleError(error); + } finally { + cleanup(); + } +} + +function riskyOperation() { + throw new Error("test"); +} + +function handleError(e: any) {} +function cleanup() {} + +// Async/await patterns +async function asyncCalls() { + await fetchData(); // Await call + + const result = await Promise.all([ + asyncOp1(), // Call in array + asyncOp2(), + ]); + + return result; +} + +async function fetchData() { + return {}; +} + +async function asyncOp1() { + return 1; +} + +async function asyncOp2() { + return 2; +} + +// Generator functions +function* generatorCalls() { + yield getValue(); // Call in yield + yield* otherGenerator(); // Delegated generator +} + +function getValue() { + return 42; +} + +function* otherGenerator() { + yield 1; + yield 2; +} + +// Destructuring with calls +function destructuringCalls() { + const { a, b } = getObject(); + const [x, y] = getArray(); +} + +function getObject() { + return { a: 1, b: 2 }; +} + +function getArray() { + return [1, 2]; +} + +// Spread operator with calls +function spreadCalls() { + const arr = [...getArray()]; + const obj = { ...getObject() }; + + combine(...getArgs()); // Spread in call args +} + +function combine(...args: any[]) { + return args; +} + +function getArgs() { + return [1, 2, 3]; +} + +// Tagged template literals +function taggedTemplate() { + const result = myTag`template ${getValue()} string`; +} + +function myTag(strings: TemplateStringsArray, ...values: any[]) { + return strings[0] + values[0]; +} diff --git a/tests/fixtures/call-graph/imports.ts b/tests/fixtures/call-graph/imports.ts new file mode 100644 index 0000000..0e7a0d1 --- /dev/null +++ b/tests/fixtures/call-graph/imports.ts @@ -0,0 +1,45 @@ +// Import statements test fixture +// Tests various import patterns that represent cross-file call edges + +// Named imports +import { parseFile, hashContent } from "../native"; +import { Indexer } from "../indexer/index"; +import { Logger } from "../utils/logger"; + +// Default import +import Database from "../database"; + +// Namespace import +import * as path from "path"; +import * as fs from "fs"; + +// Type-only imports (should not create call edges) +import type { CodeChunk } from "../types"; +import type { Config } from "../config"; + +// Mixed imports +import React, { useState, useEffect } from "react"; + +// Dynamic import (call expression) +async function loadModule() { + const module = await import("../utils/helpers"); + return module.helperFunction(); +} + +// Re-export (not a call, but related) +export { parseFile, hashContent } from "../native"; +export * from "../types"; + +// Using imported functions +function useImports() { + const hash = hashContent("test"); // Call to imported function + const chunks = parseFile("file.ts"); // Another imported call + + const indexer = new Indexer({}); // Constructor from import + const logger = new Logger(); + + logger.info("test"); // Method on imported class instance + + const p = path.join("a", "b"); // Namespace import usage + fs.readFileSync(p); // Another namespace call +} diff --git a/tests/fixtures/call-graph/method-calls.ts b/tests/fixtures/call-graph/method-calls.ts new file mode 100644 index 0000000..d870c6a --- /dev/null +++ b/tests/fixtures/call-graph/method-calls.ts @@ -0,0 +1,47 @@ +// Method calls test fixture +// Tests object.method() and this.method() patterns + +class Calculator { + value: number = 0; + + add(n: number) { + this.value += n; + this.validate(); // this.method() call + return this; + } + + subtract(n: number) { + this.value -= n; + return this; + } + + validate() { + if (this.value < 0) { + this.reset(); // Another this.method() call + } + } + + reset() { + this.value = 0; + } +} + +// Object method calls +const calc = new Calculator(); +calc.add(5); // obj.method() call +calc.subtract(2); // Chained method call +calc.add(3).subtract(1); // Method chaining + +// Static method calls +class MathUtils { + static square(n: number) { + return n * n; + } + + static cube(n: number) { + return MathUtils.square(n) * n; // Static method calling static method + } +} + +MathUtils.square(5); // Static method call +MathUtils.cube(3); diff --git a/tests/fixtures/call-graph/nested-calls.ts b/tests/fixtures/call-graph/nested-calls.ts new file mode 100644 index 0000000..0625747 --- /dev/null +++ b/tests/fixtures/call-graph/nested-calls.ts @@ -0,0 +1,99 @@ +// Nested calls test fixture +// Tests deeply nested and complex call patterns + +function outer() { + inner(middle(deep())); // Triple-nested call + + const result = compute( + transform( + getData() // Nested in function args + ) + ); + + return result; +} + +function inner(value: any) { + return process(value); // Call in return +} + +function middle(value: any) { + return normalize(value); +} + +function deep() { + return fetch(); +} + +function compute(data: any) { + return data; +} + +function transform(data: any) { + return validate(sanitize(data)); // Nested in return +} + +function getData() { + return { value: 42 }; +} + +function process(v: any) { + return v; +} + +function normalize(v: any) { + return v; +} + +function fetch() { + return {}; +} + +function validate(v: any) { + return v; +} + +function sanitize(v: any) { + return v; +} + +// Callback nesting +function withCallback() { + doAsync((result) => { + process(result); // Call in callback + + doAsync((nested) => { + finalize(nested); // Call in nested callback + }); + }); +} + +function doAsync(cb: (result: any) => void) { + cb({}); +} + +function finalize(v: any) { + return v; +} + +// Array method chaining with calls +function chainedCalls() { + const data = [1, 2, 3]; + + return data + .map(x => square(x)) // Call in arrow function + .filter(x => isValid(x)) // Call in another arrow + .reduce((a, b) => sum(a, b)); // Call in reducer +} + +function square(n: number) { + return n * n; +} + +function isValid(n: number) { + return n > 0; +} + +function sum(a: number, b: number) { + return a + b; +} diff --git a/tests/fixtures/call-graph/same-file-refs.ts b/tests/fixtures/call-graph/same-file-refs.ts new file mode 100644 index 0000000..a482cb7 --- /dev/null +++ b/tests/fixtures/call-graph/same-file-refs.ts @@ -0,0 +1,81 @@ +// Same-file references test fixture +// Tests resolution of calls within the same file +// All calls here should be resolvable (is_resolved=true) + +function entryPoint() { + helperA(); // Should resolve to helperA below + helperB(42); // Should resolve to helperB below + + const obj = new MyClass(); // Should resolve to MyClass constructor + obj.doWork(); // Should resolve to MyClass.doWork + + MyClass.staticMethod(); // Should resolve to static method +} + +function helperA() { + helperB(10); // Should resolve to helperB + internalUtil(); // Should resolve to internalUtil +} + +function helperB(n: number) { + return n * 2; +} + +function internalUtil() { + return "util"; +} + +class MyClass { + static staticMethod() { + return "static"; + } + + doWork() { + this.privateMethod(); // Should resolve to privateMethod + return "work"; + } + + privateMethod() { + return "private"; + } +} + +// Arrow functions +const arrowFunc = () => { + helperA(); // Should resolve to helperA +}; + +// Function expressions +const funcExpr = function() { + helperB(5); // Should resolve to helperB +}; + +// Nested scopes +function outerScope() { + function innerScope() { + helperA(); // Should resolve to top-level helperA + } + + innerScope(); // Should resolve to innerScope above +} + +// Mutual recursion +function fibonacci(n: number): number { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); // Self-call, should resolve to itself +} + +function evenOdd(n: number): boolean { + if (n === 0) return true; + return isOdd(n - 1); // Should resolve to isOdd below +} + +function isOdd(n: number): boolean { + if (n === 0) return false; + return evenOdd(n - 1); // Should resolve to evenOdd above +} + +// Export doesn't change resolution +export function exported() { + helperA(); // Still resolves within same file +} diff --git a/tests/fixtures/call-graph/simple-calls.ts b/tests/fixtures/call-graph/simple-calls.ts new file mode 100644 index 0000000..3190ef1 --- /dev/null +++ b/tests/fixtures/call-graph/simple-calls.ts @@ -0,0 +1,24 @@ +// Direct function calls test fixture +// Tests basic function-to-function call patterns + +function caller() { + directCall(); // Direct function call - no args + helper(1, 2); // Call with arguments + const result = compute(data); // Call with assignment + nested.deep.call(); // Nested member access call +} + +function directCall() { + console.log("called"); +} + +function helper(a: number, b: number) { + return a + b; +} + +function compute(d: any) { + return d; +} + +const data = { value: 42 }; +const nested = { deep: { call: () => {} } }; diff --git a/tests/mcp-server.test.ts b/tests/mcp-server.test.ts new file mode 100644 index 0000000..afbffe6 --- /dev/null +++ b/tests/mcp-server.test.ts @@ -0,0 +1,307 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { createMcpServer } from "../src/mcp-server.js"; +import { parseConfig } from "../src/config/schema.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; + +vi.mock("../src/indexer/index.js", () => { + class MockIndexer { + initialize = vi.fn().mockResolvedValue(undefined); + search = vi.fn().mockResolvedValue([ + { + filePath: "src/auth.ts", + startLine: 10, + endLine: 25, + name: "validateToken", + chunkType: "function", + content: "function validateToken(token: string) {\n return token.length > 0;\n}", + score: 0.95, + }, + ]); + findSimilar = vi.fn().mockResolvedValue([ + { + filePath: "src/utils.ts", + startLine: 5, + endLine: 15, + name: "checkAuth", + chunkType: "function", + content: "function checkAuth(token: string) {\n return !!token;\n}", + score: 0.88, + }, + ]); + index = vi.fn().mockResolvedValue({ + totalFiles: 10, + totalChunks: 50, + indexedChunks: 50, + failedChunks: 0, + tokensUsed: 1000, + durationMs: 500, + existingChunks: 0, + removedChunks: 0, + skippedFiles: [], + parseFailures: [], + }); + getStatus = vi.fn().mockResolvedValue({ + indexed: true, + vectorCount: 50, + provider: "openai", + model: "text-embedding-3-small", + indexPath: "/tmp/index", + currentBranch: "main", + baseBranch: "main", + }); + healthCheck = vi.fn().mockResolvedValue({ + removed: 0, + gcOrphanEmbeddings: 0, + gcOrphanChunks: 0, + filePaths: [], + }); + clearIndex = vi.fn().mockResolvedValue(undefined); + estimateCost = vi.fn().mockResolvedValue({ + filesCount: 10, + totalSizeBytes: 50000, + estimatedChunks: 50, + estimatedTokens: 1000, + estimatedCost: 0.01, + isFree: false, + provider: "openai", + model: "text-embedding-3-small", + }); + getLogger = vi.fn().mockReturnValue({ + isEnabled: vi.fn().mockReturnValue(false), + isMetricsEnabled: vi.fn().mockReturnValue(false), + getLogs: vi.fn().mockReturnValue([]), + getLogsByCategory: vi.fn().mockReturnValue([]), + getLogsByLevel: vi.fn().mockReturnValue([]), + formatMetrics: vi.fn().mockReturnValue(""), + }); + } + return { Indexer: MockIndexer }; +}); + +describe("createMcpServer", () => { + it("should create a server instance", () => { + const config = parseConfig({}); + const server = createMcpServer("/tmp/test-project", config); + + expect(server).toBeDefined(); + expect(server).toHaveProperty("connect"); + }); + + it("should have the correct server name", () => { + const config = parseConfig({}); + const server = createMcpServer("/tmp/test-project", config); + + expect(server).toBeDefined(); + }); + +}); + +describe("MCP server tools and prompts", () => { + let client: Client; + let server: ReturnType; + + beforeEach(async () => { + const config = parseConfig({}); + server = createMcpServer("/tmp/test-project", config); + client = new Client({ name: "test-client", version: "1.0.0" }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([ + server.connect(serverTransport), + client.connect(clientTransport), + ]); + }); + + afterEach(async () => { + await client.close(); + }); + + it("should register all 8 tools", async () => { + const tools = await client.listTools(); + + expect(tools.tools).toHaveLength(8); + + const toolNames = tools.tools.map(t => t.name).sort(); + const expectedNames = [ + "codebase_peek", + "codebase_search", + "find_similar", + "index_codebase", + "index_health_check", + "index_logs", + "index_metrics", + "index_status", + ].sort(); + + expect(toolNames).toEqual(expectedNames); + }); + + it("should register all 4 prompts", async () => { + const prompts = await client.listPrompts(); + + expect(prompts.prompts).toHaveLength(4); + + const promptNames = prompts.prompts.map(p => p.name).sort(); + const expectedNames = ["find", "index", "search", "status"].sort(); + + expect(promptNames).toEqual(expectedNames); + }); + + it("should execute codebase_search tool", async () => { + const result = await client.callTool({ + name: "codebase_search", + arguments: { query: "test query" }, + }); + + expect(result.content).toBeDefined(); + const content = result.content as Array<{ type: string; text?: string }>; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + expect(content[0].text).toContain("Found 1 results"); + expect(content[0].text).toContain("validateToken"); + }); + + it("should execute codebase_peek tool", async () => { + const result = await client.callTool({ + name: "codebase_peek", + arguments: { query: "test query" }, + }); + + expect(result.content).toBeDefined(); + const content = result.content as Array<{ type: string; text?: string }>; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + expect(content[0].text).toContain("Found 1 locations"); + }); + + it("should execute index_status tool", async () => { + const result = await client.callTool({ + name: "index_status", + arguments: {}, + }); + + expect(result.content).toBeDefined(); + const content = result.content as Array<{ type: string; text?: string }>; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + expect(content[0].text).toContain("Index status"); + expect(content[0].text).toContain("50"); + }); + + it("should execute index_codebase with estimateOnly", async () => { + const result = await client.callTool({ + name: "index_codebase", + arguments: { estimateOnly: true }, + }); + + expect(result.content).toBeDefined(); + const content = result.content as Array<{ type: string; text?: string }>; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + expect(content[0].text).toContain("Estimate"); + }); + + it("should execute index_health_check tool", async () => { + const result = await client.callTool({ + name: "index_health_check", + arguments: {}, + }); + + expect(result.content).toBeDefined(); + const content = result.content as Array<{ type: string; text?: string }>; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + expect(content[0].text).toContain("healthy"); + }); + + it("should execute find_similar tool", async () => { + const result = await client.callTool({ + name: "find_similar", + arguments: { code: "function test() {}" }, + }); + + expect(result.content).toBeDefined(); + const content = result.content as Array<{ type: string; text?: string }>; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + expect(content[0].text).toContain("Found 1 similar"); + }); + + it("should get search prompt", async () => { + const prompt = await client.getPrompt({ + name: "search", + arguments: { query: "auth logic" }, + }); + + expect(prompt.messages).toBeDefined(); + expect(prompt.messages).toHaveLength(1); + expect(prompt.messages[0].role).toBe("user"); + const msgContent = prompt.messages[0].content as { type: string; text?: string }; + expect(msgContent.type).toBe("text"); + expect(msgContent.text).toContain("auth logic"); + }); + + it("should get find prompt", async () => { + const prompt = await client.getPrompt({ + name: "find", + arguments: { query: "validation" }, + }); + + expect(prompt.messages).toBeDefined(); + expect(prompt.messages).toHaveLength(1); + expect(prompt.messages[0].role).toBe("user"); + const msgContent = prompt.messages[0].content as { type: string; text?: string }; + expect(msgContent.text).toContain("validation"); + }); + + it("should get index prompt", async () => { + const prompt = await client.getPrompt({ + name: "index", + arguments: {}, + }); + + expect(prompt.messages).toBeDefined(); + expect(prompt.messages).toHaveLength(1); + expect(prompt.messages[0].role).toBe("user"); + const msgContent = prompt.messages[0].content as { type: string; text?: string }; + expect(msgContent.text).toContain("index_codebase"); + }); + + it("should get status prompt", async () => { + const prompt = await client.getPrompt({ + name: "status", + arguments: {}, + }); + + expect(prompt.messages).toBeDefined(); + expect(prompt.messages).toHaveLength(1); + expect(prompt.messages[0].role).toBe("user"); + const msgContent = prompt.messages[0].content as { type: string; text?: string }; + expect(msgContent.text).toContain("index_status"); + }); + + it("should execute index_metrics tool", async () => { + const result = await client.callTool({ + name: "index_metrics", + arguments: {}, + }); + + expect(result.content).toBeDefined(); + const content = result.content as Array<{ type: string; text?: string }>; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + }); + + it("should execute index_logs tool", async () => { + const result = await client.callTool({ + name: "index_logs", + arguments: {}, + }); + + expect(result.content).toBeDefined(); + const content = result.content as Array<{ type: string; text?: string }>; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + }); +}); diff --git a/tsup.config.ts b/tsup.config.ts index 3d46ff1..6a2dc13 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/index.ts"], + entry: ["src/index.ts", "src/cli.ts"], format: ["esm", "cjs"], dts: false, sourcemap: true, @@ -17,6 +17,8 @@ export default defineConfig({ "tiktoken", ], external: [ + "@modelcontextprotocol/sdk", + "zod", "@opencode-ai/plugin", /^node:/, "fs", @@ -37,9 +39,18 @@ export default defineConfig({ "url", ], esbuildOptions(options, context) { - options.banner = { - js: "// opencode-codebase-index - Semantic codebase search for OpenCode", - }; + if (context.format === "esm") { + options.banner = { + js: [ + "// opencode-codebase-index - Semantic codebase search for OpenCode", + "import { createRequire } from 'module'; const require = createRequire(import.meta.url);", + ].join("\n"), + }; + } else { + options.banner = { + js: "// opencode-codebase-index - Semantic codebase search for OpenCode", + }; + } if (context.format === "cjs") { options.logOverride = { "empty-import-meta": "silent",