opengrok-mcp-server 3.3.5 β†’ 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## Highlights
9
9
 
10
+ ### πŸ—οΈ v4.0 β€” Modern MCP SDK & Breaking Tool Rename
11
+
12
+ McpServer high-level API, `opengrok_` prefixed tool names, tool annotations, structured output, `response_format` parameter, security hardening. Full protocol compliance.
13
+
10
14
  ### 🧠 v3.0 β€” Code Intelligence Engine
11
15
 
12
16
  6 new compound tools, ~92% fewer tokens, full OpenGrok 1.7.x support, and a zero-config local source layer that knows your compiler flags. The largest update since the original rewrite.
@@ -23,14 +27,75 @@ Native MCP integration, OS keychain credentials, 8 OpenGrok tools, SSRF protecti
23
27
 
24
28
  ---
25
29
 
30
+ ## [4.0.0] - 2026-03-15
31
+
32
+ ### ⚠️ Breaking Changes
33
+
34
+ - **Tool rename**: All 14 tools now use `opengrok_` prefix for namespace clarity per MCP best practices. Update any client configurations or scripts that reference tool names.
35
+
36
+ #### Migration Guide
37
+
38
+ | Old Name (v3.x) | New Name (v4.0) |
39
+ | ---------------- | --------------- |
40
+ | `search_code` | `opengrok_search_code` |
41
+ | `find_file` | `opengrok_find_file` |
42
+ | `get_file_content` | `opengrok_get_file_content` |
43
+ | `get_file_history` | `opengrok_get_file_history` |
44
+ | `browse_directory` | `opengrok_browse_directory` |
45
+ | `list_projects` | `opengrok_list_projects` |
46
+ | `get_file_annotate` | `opengrok_get_file_annotate` |
47
+ | `search_suggest` | `opengrok_search_suggest` |
48
+ | `batch_search` | `opengrok_batch_search` |
49
+ | `search_and_read` | `opengrok_search_and_read` |
50
+ | `get_symbol_context` | `opengrok_get_symbol_context` |
51
+ | `index_health` | `opengrok_index_health` |
52
+ | `get_compile_info` | `opengrok_get_compile_info` |
53
+ | `get_file_symbols` | `opengrok_get_file_symbols` |
54
+
55
+ - **Removed `zod-to-json-schema` dependency**: `McpServer.registerTool()` handles Zod→JSON Schema natively. No action needed unless you imported from `tool-schemas.ts`.
56
+ - **Deleted `tool-schemas.ts`**: Tool definitions are now registered inline via `registerTool()` in `server.ts`.
57
+
58
+ ### Added
59
+
60
+ - **McpServer high-level API** (Phase 2): Migrated from deprecated `Server` + `setRequestHandler()` to `McpServer` + `registerTool()`. Each tool is self-contained with its own error handling.
61
+ - **Tool annotations** on all 14 tools: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` per MCP spec. `opengrok_get_compile_info` uses `openWorldHint: false` (local filesystem only).
62
+ - **`title` field** on all tool registrations for human-readable display.
63
+ - **`isError: true`** on all error responses per MCP spec. All three error types (ZodError, Error, unknown) handled via shared `makeToolError()` helper.
64
+ - **Structured output schemas** (`structuredContent`) for priority tools: `opengrok_search_code`, `opengrok_get_file_content`, `opengrok_list_projects`, `opengrok_batch_search`, `opengrok_get_symbol_context`. Programmatic clients receive typed data alongside text.
65
+ - **`response_format` parameter** on all 14 tools: `"markdown"` (default, LLM-optimised) or `"json"` (programmatic). Shared `formatResponse()` helper eliminates per-handler duplication.
66
+ - **Expanded tool descriptions** for compound tools (`opengrok_batch_search`, `opengrok_search_and_read`, `opengrok_get_symbol_context`, `opengrok_get_compile_info`, `opengrok_get_file_symbols`) with "when to use / when not to use", Args, and Example sections.
67
+ - **Output Zod schemas** in `models.ts`: `SearchResultsOutput`, `FileContentOutput`, `ProjectsListOutput`, `BatchSearchOutput`, `SymbolContextOutput` with pagination fields (`hasMore`, `nextOffset`).
68
+
69
+ ### Security
70
+
71
+ - **Logger credential redaction**: `sanitizeMeta()` in `logger.ts` strips Basic auth, Bearer tokens, URL-embedded credentials, and filesystem paths from all log output.
72
+ - **Complete HTML entity decoding**: `stripHtmlTags()` now handles decimal (`<`) and hex (`<`) numeric references, plus missing named entities (`nbsp`, `apos`).
73
+ - **Strengthened path traversal validation**: `assertSafePath()` rejects URL-encoded (`%2e%2e`, `%2f..`), double-encoded (`%252e`), and null-byte (`\0`, `%00`, `%2500`) traversal variants with decode-before-validate.
74
+ - **Deterministic cache keys**: `search()` cache uses `[...projects].sort().join(",")` instead of `JSON.stringify()`.
75
+ - **Plaintext HTTP warning**: `runServer()` logs warning when credentials are configured with `http://` base URL.
76
+
77
+ ### Changed
78
+
79
+ - **ESLint strict preset**: `tseslint.configs.strict` with `no-explicit-any: "error"`, `no-floating-promises`, `await-thenable`, `no-misused-promises`.
80
+ - **TypeScript declarations**: `tsconfig.json` enables `declaration` and `declarationMap`. `package.json` exports `types` field.
81
+ - **Coverage thresholds**: `vitest.config.ts` enforces 90% lines/functions/statements, 85% branches.
82
+ - **Structured logging**: ISO timestamps, `[INFO]`/`[WARN]`/`[ERROR]`/`[DEBUG]` prefixes, debug level gated by `OPENGROK_LOG_LEVEL`.
83
+ - **Expanded language map**: Added `.tsx`, `.jsx`, `.vue`, `.scala`, `.gradle`, `.dart`, `.zig`, `.lua`, `.r`, `.m`, `.mm`, `.pl`, `.tf`, `.toml`, `.ini`, `.proto`.
84
+ - **npm scripts**: Added `typecheck`, `lint:fix`, `validate`.
85
+ - **Magic numbers extracted**: `MAX_REDIRECTS`, `MAX_FILTER_LENGTH`, `TIMEOUTS` as named constants.
86
+ - **Non-null assertions eliminated**: All `.pop()!` replaced with `?? ""` fallback.
87
+ - **Regex patterns extracted**: `LINE_ANCHOR_RE`, `DEF_SYMBOL_RE`, `SIG_RE` as module-level constants.
88
+
89
+ ---
90
+
26
91
  ## [3.3.5] - 2026-03-15
27
92
 
28
93
  ### Changed
29
94
 
30
95
  - **Rebrand**: Extension `displayName` renamed to **OpenGrok MCP Server**.
31
- - **Description**: Updated to "MCP server bridging OpenGrok search engines with AI for deep, instant context across massive codebases" across `package.json`, `server.json`, and `README.md`.
96
+ - **Description**: Updated to "MCP server bridging OpenGrok search engine with AI for deep, instant context across massive codebases" across `package.json`, `server.json`, and `README.md`.
32
97
  - **License**: Added `LICENSE-COMMERCIAL.md` clearly describing commercial/enterprise licensing terms. Updated `LICENSE` Required Notice with author attribution. README license section now explicitly states commercial use restrictions with contact info.
33
- - **Installation docs**: README now lists npm (`npx opengrok-mcp-server`) and MCP Registry (`io.github.IcyHot09/opengrok`) as first-class installation options alongside VS Code Marketplace.
98
+ - **Installation docs**: README now lists npm (`npx opengrok-mcp-server`) and MCP Registry (`io.github.IcyHot09/opengrok-mcp-server`) as first-class installation options alongside VS Code Marketplace.
34
99
  - **MCP Registry badge**: Added to README header badges.
35
100
  - **Release tooling**: Fixed wrong GitHub repo URL in `generate-release-notes.js`. Release script now syncs `server.json` version on each release.
36
101
 
package/CONTRIBUTING.md CHANGED
@@ -8,8 +8,7 @@ opengrokmcp-standalone/
8
8
  β”‚ β”œβ”€β”€ extension.ts # VS Code extension entry point
9
9
  β”‚ β”œβ”€β”€ server/ # MCP Server (TypeScript)
10
10
  β”‚ β”‚ β”œβ”€β”€ main.ts # Server entry point
11
- β”‚ β”‚ β”œβ”€β”€ server.ts # MCP protocol handler + tool dispatch
12
- β”‚ β”‚ β”œβ”€β”€ tool-schemas.ts # Zod β†’ JSON Schema tool definitions
11
+ β”‚ β”‚ β”œβ”€β”€ server.ts # McpServer with per-tool registerTool() handlers
13
12
  β”‚ β”‚ β”œβ”€β”€ client.ts # OpenGrok HTTP client
14
13
  β”‚ β”‚ β”œβ”€β”€ config.ts # Env-var config (Zod-validated)
15
14
  β”‚ β”‚ β”œβ”€β”€ models.ts # Zod schemas + TypeScript interfaces
@@ -98,7 +97,7 @@ User (Copilot Chat)
98
97
  | Zod 4 for config + validation | Type-safe parsing of env vars and tool args |
99
98
  | `p-retry` for retries | Configurable exponential backoff |
100
99
  | TTL cache with byte budget | Prevents OOM from large file caching |
101
- | Compound tools (`get_symbol_context`, `search_and_read`, `batch_search`) | Collapse multi-step patterns into a single call β€” ~75–92% token savings |
100
+ | Compound tools (`opengrok_get_symbol_context`, `opengrok_search_and_read`, `opengrok_batch_search`) | Collapse multi-step patterns into a single call β€” ~75–92% token savings |
102
101
  | HTML fallback parsing | Gracefully handle OpenGrok instances where REST API is disabled |
103
102
  | 16 KB response cap | Prevents blowing up Copilot's context window |
104
103
 
@@ -112,22 +111,33 @@ User (Copilot Chat)
112
111
  export const MyNewToolArgs = z.object({
113
112
  param1: z.string().min(1),
114
113
  param2: z.number().int().default(10),
114
+ response_format: RESPONSE_FORMAT,
115
115
  });
116
116
  ```
117
117
 
118
- 2. **Add tool definition** to `TOOL_DEFINITIONS` in `src/server/tool-schemas.ts`
119
-
120
- 3. **Add dispatch case** in `dispatchTool()` in `src/server/server.ts`:
118
+ 2. **Register the tool** via `server.registerTool()` in `src/server/server.ts`:
121
119
 
122
120
  ```typescript
123
- case "my_new_tool": {
124
- const args = MyNewToolArgs.parse(rawArgs);
125
- const result = await client.myMethod(args.param1, args.param2);
126
- return formatMyResult(result);
127
- }
121
+ server.registerTool(
122
+ "opengrok_my_new_tool",
123
+ {
124
+ title: "My New Tool",
125
+ description: "Description of what this tool does.",
126
+ inputSchema: MyNewToolArgs.shape,
127
+ annotations: READ_ONLY_OPEN,
128
+ },
129
+ async (args) => {
130
+ try {
131
+ const result = await client.myMethod(args.param1, args.param2);
132
+ return { content: [{ type: "text", text: capResponse(formatMyResult(result)) }] };
133
+ } catch (err) {
134
+ return makeToolError("opengrok_my_new_tool", err);
135
+ }
136
+ }
137
+ );
128
138
  ```
129
139
 
130
- 4. **Add client method** in `src/server/client.ts`:
140
+ 3. **Add client method** in `src/server/client.ts`:
131
141
 
132
142
  ```typescript
133
143
  async myMethod(param1: string, param2: number): Promise<MyType> {
@@ -138,9 +148,9 @@ async myMethod(param1: string, param2: number): Promise<MyType> {
138
148
  }
139
149
  ```
140
150
 
141
- 5. **Add formatter** in `src/server/formatters.ts` for the Markdown output
151
+ 4. **Add formatter** in `src/server/formatters.ts` for the Markdown output
142
152
 
143
- 6. **Add unit tests** in `src/tests/`
153
+ 5. **Add unit tests** in `src/tests/`
144
154
 
145
155
  ---
146
156
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # OpenGrok MCP Server
6
6
 
7
- **MCP server bridging OpenGrok code search engine with AI for deep, instant context across massive codebases**
7
+ **MCP server bridging OpenGrok search engine with AI for instant context across massive codebases**
8
8
 
9
9
  [![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/IcyHot09.opengrok-mcp-server?label=VS%20Code%20Marketplace&logo=visualstudiocode)](https://marketplace.visualstudio.com/items?itemName=IcyHot09.opengrok-mcp-server) [![Installs](https://img.shields.io/visual-studio-marketplace/i/IcyHot09.opengrok-mcp-server)](https://marketplace.visualstudio.com/items?itemName=IcyHot09.opengrok-mcp-server) [![npm](https://img.shields.io/npm/v/opengrok-mcp-server?logo=npm)](https://www.npmjs.com/package/opengrok-mcp-server) [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-blue)](https://registry.modelcontextprotocol.io) [![CI](https://github.com/IcyHot09/opengrok-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/IcyHot09/opengrok-mcp-server/actions/workflows/ci.yml) [![GitHub Release](https://img.shields.io/github/v/release/IcyHot09/opengrok-mcp-server)](https://github.com/IcyHot09/opengrok-mcp-server/releases)
10
10
 
@@ -58,7 +58,7 @@ npm install -g opengrok-mcp-server
58
58
 
59
59
  ### Option 3 β€” MCP Registry
60
60
 
61
- This server is listed on the [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.IcyHot09/opengrok`. Any MCP-compatible client that supports the registry can discover and install it automatically.
61
+ This server is listed on the [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.IcyHot09/opengrok-mcp-server`. Any MCP-compatible client that supports the registry can discover and install it automatically.
62
62
 
63
63
  ### Option 4 β€” Install pre-built VSIX
64
64
 
@@ -133,15 +133,15 @@ Search for all references to TaskScheduler across the codebase
133
133
 
134
134
  | Tool | Description |
135
135
  | ---- | ----------- |
136
- | `search_code` | Full-text, symbol definition, reference, path, or commit message search. Optional `file_type` filter. |
137
- | `find_file` | Find files by path or name pattern. |
138
- | `get_file_content` | Retrieve file contents β€” pass `start_line`/`end_line` to limit output. |
139
- | `get_file_history` | Commit history for a file. |
140
- | `browse_directory` | List directory contents. |
141
- | `list_projects` | List all accessible projects. |
142
- | `get_file_annotate` | Git blame with optional line range. |
143
- | `get_file_symbols` | List all top-level symbols in a file. |
144
- | `search_suggest` | Autocomplete/suggestions for partial queries. |
136
+ | `opengrok_search_code` | Full-text, symbol definition, reference, path, or commit message search. Optional `file_type` filter. |
137
+ | `opengrok_find_file` | Find files by path or name pattern. |
138
+ | `opengrok_get_file_content` | Retrieve file contents β€” pass `start_line`/`end_line` to limit output. |
139
+ | `opengrok_get_file_history` | Commit history for a file. |
140
+ | `opengrok_browse_directory` | List directory contents. |
141
+ | `opengrok_list_projects` | List all accessible projects. |
142
+ | `opengrok_get_file_annotate` | Git blame with optional line range. |
143
+ | `opengrok_get_file_symbols` | List all top-level symbols in a file. |
144
+ | `opengrok_search_suggest` | Autocomplete/suggestions for partial queries. |
145
145
 
146
146
  ### πŸš€ Compound Tools β€” Use These First
147
147
 
@@ -149,19 +149,19 @@ Search for all references to TaskScheduler across the codebase
149
149
 
150
150
  | Tool | What it replaces | Token savings |
151
151
  | ---- | ---------------- | ------------- |
152
- | `get_symbol_context` | `search_code(defs)` β†’ `get_file_content` β†’ `search_code(refs)` | **~92%** |
153
- | `search_and_read` | `search_code` β†’ `get_file_content` | **~92%** |
154
- | `batch_search` | Multiple sequential `search_code` calls | **~73%** |
155
- | `index_health` | Manual connection diagnostics | β€” |
152
+ | `opengrok_get_symbol_context` | `opengrok_search_code(defs)` β†’ `opengrok_get_file_content` β†’ `opengrok_search_code(refs)` | **~92%** |
153
+ | `opengrok_search_and_read` | `opengrok_search_code` β†’ `opengrok_get_file_content` | **~92%** |
154
+ | `opengrok_batch_search` | Multiple sequential `opengrok_search_code` calls | **~73%** |
155
+ | `opengrok_index_health` | Manual connection diagnostics | β€” |
156
156
 
157
- **`file_type` filter** β€” `search_code`, `batch_search`, `search_and_read`, and `get_symbol_context` accept an optional `file_type` to restrict results by language: `cxx`, `c`, `java`, `python`, `javascript`, `typescript`, `csharp`, `golang`, `ruby`, `perl`, `sql`, `xml`, `yaml`, `shell`, `makefile`.
157
+ **`file_type` filter** β€” `opengrok_search_code`, `opengrok_batch_search`, `opengrok_search_and_read`, and `opengrok_get_symbol_context` accept an optional `file_type` to restrict results by language: `cxx`, `c`, `java`, `python`, `javascript`, `typescript`, `csharp`, `golang`, `ruby`, `perl`, `sql`, `xml`, `yaml`, `shell`, `makefile`.
158
158
 
159
159
  <details>
160
160
  <summary>βš™οΈ Local Source Layer (Optional)</summary>
161
161
 
162
162
  | Tool | Description |
163
163
  | ---- | ----------- |
164
- | `get_compile_info` | Compiler flags, include paths, defines, and language standard for a source file. Requires `compile_commands.json` in your workspace. |
164
+ | `opengrok_get_compile_info` | Compiler flags, include paths, defines, and language standard for a source file. Requires `compile_commands.json` in your workspace. |
165
165
 
166
166
  </details>
167
167
 
@@ -243,6 +243,28 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development guide.
243
243
 
244
244
  ---
245
245
 
246
+ ## Troubleshooting
247
+
248
+ | Problem | Solution |
249
+ | ------- | -------- |
250
+ | Tools not appearing in Copilot | Click **πŸ”§ Tools** β†’ **Update Tools** in the chat input. Reload the window (`Ctrl+Shift+P` β†’ "Reload Window"). |
251
+ | "Connection failed" on test | Verify `OPENGROK_BASE_URL` is reachable. Check proxy settings if behind a corporate firewall. |
252
+ | 401 Unauthorized | Set `OPENGROK_USERNAME` and `OPENGROK_PASSWORD` via `OpenGrok: Configure Credentials`. |
253
+ | Results seem stale | Call `opengrok_index_health` to check connectivity. OpenGrok index may need a rebuild by an admin. |
254
+ | SSL certificate errors | Set `opengrok-mcp.verifySsl` to `false` for self-signed or internal CA certificates. |
255
+ | Timeouts on large repos | Narrow queries with `file_type`, specific projects, or smaller `max_results`. |
256
+ | Debug mode | Set `OPENGROK_LOG_LEVEL=debug` environment variable to enable verbose logging to stderr. |
257
+
258
+ ### OpenGrok Version Compatibility
259
+
260
+ | OpenGrok Version | Support Level | Notes |
261
+ | ---------------- | ------------- | ----- |
262
+ | 1.13+ | Full | REST API + HTML fallback |
263
+ | 1.7.x – 1.12.x | Full | HTML parsing fallback for `defs`/`refs` search, annotate |
264
+ | < 1.7 | Untested | May work with limited functionality |
265
+
266
+ ---
267
+
246
268
  ## License
247
269
 
248
270
  This project is licensed under the [PolyForm Noncommercial License 1.0.0](LICENSE).
package/evaluation.xml ADDED
@@ -0,0 +1,88 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ Evaluation file for the OpenGrok MCP Server.
4
+ 10 complex read-only questions exercising multi-tool compositions.
5
+ Use with MCP Inspector or automated evaluation harness.
6
+ -->
7
+ <evaluation>
8
+ <meta>
9
+ <name>OpenGrok MCP Server v4.0 Evaluation</name>
10
+ <version>4.0.0</version>
11
+ <description>
12
+ Tests for multi-tool composition, structured output, response_format,
13
+ error handling, and compound tool usage across all 14 opengrok_ tools.
14
+ </description>
15
+ </meta>
16
+
17
+ <questions>
18
+ <!-- Q1: Basic search + file content composition -->
19
+ <question id="1" difficulty="medium">
20
+ <prompt>Find the definition of the EventLoop class and show me the first 30 lines of the file where it's defined.</prompt>
21
+ <expected_tools>opengrok_search_code, opengrok_get_file_content</expected_tools>
22
+ <verify>Response includes file path, line numbers, and code block with class definition</verify>
23
+ </question>
24
+
25
+ <!-- Q2: Compound tool β€” get_symbol_context replaces multi-step -->
26
+ <question id="2" difficulty="easy">
27
+ <prompt>What is the GridNode::processMessage function? Show me its definition, any header declaration, and where it's called from.</prompt>
28
+ <expected_tools>opengrok_get_symbol_context</expected_tools>
29
+ <verify>Response includes definition context, header (if .cpp), and reference samples</verify>
30
+ </question>
31
+
32
+ <!-- Q3: Compound tool β€” batch_search for parallel investigation -->
33
+ <question id="3" difficulty="medium">
34
+ <prompt>Search for all definitions of "ConnectionPool", all references to "ConnectionPool", and all path matches for files named "*connection*" β€” do it in one call.</prompt>
35
+ <expected_tools>opengrok_batch_search</expected_tools>
36
+ <verify>Response contains three query results with match counts for each</verify>
37
+ </question>
38
+
39
+ <!-- Q4: Compound tool β€” search_and_read for contextual results -->
40
+ <question id="4" difficulty="easy">
41
+ <prompt>Find where "SIGTERM" is handled in the codebase and show me the surrounding code.</prompt>
42
+ <expected_tools>opengrok_search_and_read</expected_tools>
43
+ <verify>Response includes code context around SIGTERM matches without full-file fetch</verify>
44
+ </question>
45
+
46
+ <!-- Q5: File exploration pipeline -->
47
+ <question id="5" difficulty="hard">
48
+ <prompt>List all projects, then browse the root directory of the first project, then show me the symbols in its main entry file.</prompt>
49
+ <expected_tools>opengrok_list_projects, opengrok_browse_directory, opengrok_get_file_symbols</expected_tools>
50
+ <verify>Pipeline: projects β†’ directory listing β†’ symbol list with function/class names</verify>
51
+ </question>
52
+
53
+ <!-- Q6: History + blame composition -->
54
+ <question id="6" difficulty="medium">
55
+ <prompt>Show me the last 5 commits for EventLoop.cpp and then show blame annotations for lines 50-80.</prompt>
56
+ <expected_tools>opengrok_get_file_history, opengrok_get_file_annotate</expected_tools>
57
+ <verify>History entries with revisions + blame annotations with author/revision per line</verify>
58
+ </question>
59
+
60
+ <!-- Q7: response_format=json structured output -->
61
+ <question id="7" difficulty="medium">
62
+ <prompt>Search for "mutex" with response_format set to "json" and verify the output is valid JSON with totalCount and results array.</prompt>
63
+ <expected_tools>opengrok_search_code</expected_tools>
64
+ <verify>Text content is valid JSON; structuredContent contains totalCount, results array, hasMore</verify>
65
+ </question>
66
+
67
+ <!-- Q8: Error handling β€” invalid arguments -->
68
+ <question id="8" difficulty="easy">
69
+ <prompt>Call opengrok_search_code with an empty query string and verify the error response has isError=true.</prompt>
70
+ <expected_tools>opengrok_search_code</expected_tools>
71
+ <verify>Response has isError: true and contains "Invalid arguments" message</verify>
72
+ </question>
73
+
74
+ <!-- Q9: Local layer β€” compile_commands.json -->
75
+ <question id="9" difficulty="hard">
76
+ <prompt>Get the compiler flags and include paths for GridNode/EventLoop.cpp using the local compile info tool.</prompt>
77
+ <expected_tools>opengrok_get_compile_info</expected_tools>
78
+ <verify>Response includes compiler, flags, includes, and defines (or "not enabled" message)</verify>
79
+ </question>
80
+
81
+ <!-- Q10: Multi-step investigation with narrowing -->
82
+ <question id="10" difficulty="hard">
83
+ <prompt>Find all files matching "*Config*.java", then get the symbols from the first result, then read the constructor definition using the line number from the symbols list.</prompt>
84
+ <expected_tools>opengrok_find_file, opengrok_get_file_symbols, opengrok_get_file_content</expected_tools>
85
+ <verify>Three-step pipeline: find_file β†’ get_file_symbols β†’ targeted get_file_content with start_line/end_line</verify>
86
+ </question>
87
+ </questions>
88
+ </evaluation>
package/out/extension.js CHANGED
@@ -1 +1 @@
1
- "use strict";var J=Object.create;var U=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var Q=Object.getOwnPropertyNames;var X=Object.getPrototypeOf,j=Object.prototype.hasOwnProperty;var Z=(e,n)=>{for(var t in n)U(e,t,{get:n[t],enumerable:!0})},B=(e,n,t,a)=>{if(n&&typeof n=="object"||typeof n=="function")for(let s of Q(n))!j.call(e,s)&&s!==t&&U(e,s,{get:()=>n[s],enumerable:!(a=Y(n,s))||a.enumerable});return e};var P=(e,n,t)=>(t=e!=null?J(X(e)):{},B(n||!e||!e.__esModule?U(t,"default",{value:e,enumerable:!0}):t,e)),ee=e=>B(U({},"__esModule",{value:!0}),e);var ke={};Z(ke,{activate:()=>ce,deactivate:()=>Ce});module.exports=ee(ke);var o=P(require("vscode")),g=P(require("fs")),x=P(require("http")),T=P(require("https")),M=P(require("os")),$=P(require("path")),E=P(require("crypto")),H=require("child_process");var oe=6e4,N="IcyHot09",W="opengrok-mcp-server",ne="https://api.github.com",te=`https://github.com/${N}/${W}/releases`,se=1440*60*1e3,G,w,S,R,I=!1;function re(){try{let e=M.tmpdir(),n=g.readdirSync(e),t=Date.now();for(let a of n)if(a.startsWith("opengrok-cred-")&&a.endsWith(".tmp")){let s=$.join(e,a);try{let i=g.statSync(s),r=t-i.mtimeMs;r>oe&&(g.unlinkSync(s),p(`Cleaned up stale credential file (${Math.round(r/1e3)}s old): ${s}`))}catch{}}}catch(e){p(`Warning: Failed to clean up stale credential files: ${e}`)}}function ae(){try{let e=M.tmpdir(),n=g.readdirSync(e);for(let t of n)if(t.startsWith("opengrok-cred-")&&t.endsWith(".tmp")){let a=$.join(e,t);try{g.unlinkSync(a),p(`Credential file cleaned up: ${a}`)}catch{}}}catch(e){p(`Warning: Failed to clean up credential files: ${e}`)}}function _(){return o.extensions.getExtension("IcyHot09.opengrok-mcp-server")?.packageJSON.version||"0.0.0"}function q(){R&&(R.fireChanged(),p("Notified VS Code of MCP server definition change."))}async function ie(e){let n=_(),t=e.globalState.get("extensionVersion");if(await e.globalState.update("extensionVersion",n),t&&t!==n){p(`Extension updated: ${t} \u2192 ${n}`);let a=await o.window.showInformationMessage(`OpenGrok MCP updated to v${n}! To use the latest features in Copilot Chat, please reload the window, then enable OpenGrok in the \u{1F527} Tools menu.`,"Reload Window","View Changelog","Later");a==="Reload Window"?o.commands.executeCommand("workbench.action.reloadWindow"):a==="View Changelog"&&o.env.openExternal(o.Uri.parse(`https://github.com/${N}/${W}/blob/main/CHANGELOG.md`)),q()}}async function F(e,n){let t=n?.manual??!1,s=o.workspace.getConfiguration("opengrok-mcp").get("verifySsl")??!0;if(!t){let i=e.globalState.get("lastUpdateCheck",0);if(Date.now()-i<se){p("Update check skipped \u2014 checked recently.");return}}try{p("Checking for extension updates...");let i=new URL(`${ne}/repos/${N}/${W}/releases?per_page=5`),l=await ge(i,{"User-Agent":"OpenGrok-MCP-UpdateCheck/1.0",Accept:"application/vnd.github+json"},s);await e.globalState.update("lastUpdateCheck",Date.now());let c=null,d="";for(let f of l){if(f.prerelease||f.draft)continue;let h=(f.tag_name??"").replace(/^v/,"");!h||/beta|alpha|rc/i.test(h)||(!d||V(h,d)>0)&&(d=h,c=f)}if(!d){p("No stable version found in releases."),t&&o.window.showInformationMessage("OpenGrok MCP: No stable release found.");return}let u=_();if(p(`Update check: installed=${u}, latest=${d}`),V(d,u)<=0){p("Already on the latest version."),t&&o.window.showInformationMessage(`OpenGrok MCP: You are on the latest version (v${u}).`);return}let y=c.assets?.find(f=>/\.vsix$/i.test(f.name??""))?.browser_download_url,v=`${te}/tag/v${d}`,b=["View Release Notes","Dismiss"];y&&b.unshift("Install Update");let m=await o.window.showInformationMessage(`OpenGrok MCP v${d} is available (you have v${u}).`,...b);if(m==="Install Update"&&y){let f=$.join(M.tmpdir(),`opengrok-mcp-${d}.vsix`);await o.window.withProgress({location:o.ProgressLocation.Notification,title:`Downloading OpenGrok MCP v${d}...`,cancellable:!1},async()=>{await K(new URL(y),f,s)}),await o.commands.executeCommand("workbench.extensions.installExtension",o.Uri.file(f));try{g.unlinkSync(f)}catch{}await o.window.showInformationMessage(`OpenGrok MCP updated to v${d}! Reload to activate the new version.`,"Reload Window")==="Reload Window"&&o.commands.executeCommand("workbench.action.reloadWindow")}else m==="View Release Notes"&&o.env.openExternal(o.Uri.parse(v))}catch(i){p(`Update check failed (non-fatal): ${i?.message??i}`),t&&o.window.showWarningMessage(`OpenGrok MCP: Could not check for updates. ${i?.message??""}`)}}async function ce(e){S=e.secrets,G=o.window.createOutputChannel("OpenGrok MCP"),e.subscriptions.push(G),w=o.window.createStatusBarItem(o.StatusBarAlignment.Right,100),w.command="opengrok-mcp.showLogs",e.subscriptions.push(w);let n=_();p(`OpenGrok MCP v${n} activating...`),await ie(e),e.subscriptions.push(o.commands.registerCommand("opengrok-mcp.configure",pe),o.commands.registerCommand("opengrok-mcp.configureUI",()=>A(e)),o.commands.registerCommand("opengrok-mcp.test",D),o.commands.registerCommand("opengrok-mcp.showLogs",()=>G.show()),o.commands.registerCommand("opengrok-mcp.statusMenu",de),o.commands.registerCommand("opengrok-mcp.checkUpdate",()=>F(e,{manual:!0}))),F(e);let t=o.workspace.getConfiguration("opengrok-mcp"),a=t.get("username");o.lm&&o.lm.registerMcpServerDefinitionProvider?(R=new L,e.subscriptions.push(o.lm.registerMcpServerDefinitionProvider("opengrok-mcp-server",R)),p("Registered native MCP Server Definition Provider.")):p("Warning: This version of VS Code does not support native MCP Server Definition Providers. Copilot features may not work."),a?O("ready"):(I=!0,O("unconfigured"),await o.window.showInformationMessage("OpenGrok MCP: To enable Copilot codebase search, please configure your OpenGrok credentials.","Configure Now","Later")==="Configure Now"?A(e):t.get("hasPromptedConfig")||(A(e),await t.update("hasPromptedConfig",!0,o.ConfigurationTarget.Global))),p(`OpenGrok MCP v${n} activated`)}function p(e){G.appendLine(`[${new Date().toISOString()}] ${e}`)}function O(e){switch(e){case"ready":w.text="$(search) OpenGrok",w.tooltip="OpenGrok MCP: Ready - Click for options",w.backgroundColor=void 0,w.command="opengrok-mcp.statusMenu";break;case"error":w.text="$(warning) OpenGrok",w.tooltip="OpenGrok MCP: Error - Click for options",w.backgroundColor=new o.ThemeColor("statusBarItem.errorBackground"),w.command="opengrok-mcp.statusMenu";break;case"unconfigured":w.text="$(gear) OpenGrok",w.tooltip="OpenGrok MCP: Not configured - Click to setup",w.command="opengrok-mcp.configureUI";break}w.show()}async function de(){let e=await o.window.showQuickPick([{label:"$(zap) Test Connection",detail:"Verify connection to the OpenGrok server",command:"opengrok-mcp.test"},{label:"$(settings-gear) Configuration Manager",detail:"Open visual configuration panel",command:"opengrok-mcp.configureUI"},{label:"$(gear) Quick Configure",detail:"Update credentials via input prompts",command:"opengrok-mcp.configure"},{label:"$(output) Show Server Logs",detail:"View diagnostic logs for learning and debugging",command:"opengrok-mcp.showLogs"},{label:"$(cloud-download) Check for Updates",detail:"Check for new extension versions on GitHub",command:"opengrok-mcp.checkUpdate"}],{placeHolder:"OpenGrok MCP Options"});e&&o.commands.executeCommand(e.command)}async function pe(){let e=o.workspace.getConfiguration("opengrok-mcp"),n=e.get("baseUrl")||"",t=await o.window.showInputBox({prompt:"Enter OpenGrok server URL",value:n,ignoreFocusOut:!0,validateInput:r=>{try{return new URL(r),null}catch{return"Please enter a valid URL"}}});if(!t)return;let a=e.get("username")||"",s=await o.window.showInputBox({prompt:"Enter your OpenGrok username",value:a,ignoreFocusOut:!0});if(!s)return;let i=await o.window.showInputBox({prompt:"Enter your OpenGrok password",password:!0,ignoreFocusOut:!0});i&&(await e.update("baseUrl",t,o.ConfigurationTarget.Global),await e.update("username",s,o.ConfigurationTarget.Global),await S.store(`opengrok-password-${s}`,i),p(`Credentials saved for user: ${s}`),o.window.showInformationMessage("OpenGrok credentials saved securely! Copilot Chat can now search your codebase.","Test Connection").then(r=>{r==="Test Connection"&&D()}),O("ready"))}function le(){o.window.showInformationMessage("Connection successful! First-time setup requires a window reload to enable OpenGrok tools in Copilot.","Reload Window").then(e=>{e==="Reload Window"&&o.commands.executeCommand("workbench.action.reloadWindow")})}async function D(){let e=o.workspace.getConfiguration("opengrok-mcp"),n=e.get("username"),t=e.get("baseUrl"),a=e.get("verifySsl")??!0;if(!n){o.window.showErrorMessage("OpenGrok: No username configured.","Configure Now").then(i=>{i==="Configure Now"&&o.commands.executeCommand("opengrok-mcp.configureUI")});return}let s=await S.get(`opengrok-password-${n}`);if(!s){o.window.showErrorMessage("OpenGrok: No password found. Please configure credentials.","Configure Now").then(i=>{i==="Configure Now"&&o.commands.executeCommand("opengrok-mcp.configureUI")});return}await o.window.withProgress({location:o.ProgressLocation.Notification,title:"Testing OpenGrok connection...",cancellable:!1},async()=>{try{let i=t||"",r=new URL(i),l=Buffer.from(`${n}:${s}`).toString("base64"),c=await ue(r,{Authorization:`Basic ${l}`,"User-Agent":"OpenGrok-MCP/2.0.0"},a);c>=200&&c<400?(p(`Connection test successful (HTTP ${c})`),o.window.showInformationMessage("\u2713 OpenGrok connection successful!"),O("ready"),I&&(le(),I=!1)):c===401?(p("Authentication failed (401)"),o.window.showErrorMessage("\u2717 Authentication failed. Check your username and password."),O("error")):(p(`Unexpected status: ${c}`),o.window.showWarningMessage(`OpenGrok returned HTTP ${c}`))}catch(i){let r=i.message??String(i);p(`Connection test failed: ${r}`),r.includes("certificate")||r.includes("self-signed")||r.includes("CERT")||r.includes("SSL")?o.window.showErrorMessage("\u2717 SSL certificate error. If using a self-signed/internal CA, disable SSL verification in Settings.","Open Settings").then(l=>{l==="Open Settings"&&o.commands.executeCommand("workbench.action.openSettings","opengrok-mcp.verifySsl")}):o.window.showErrorMessage(`\u2717 Connection failed: ${r}`),O("error")}})}function ue(e,n,t){return new Promise((a,s)=>{let r=(e.protocol==="https:"?T:x).request({hostname:e.hostname,port:e.port||(e.protocol==="https:"?443:80),path:e.pathname+e.search,method:"GET",headers:n,rejectUnauthorized:t,timeout:15e3},l=>{l.resume(),a(l.statusCode)});r.on("timeout",()=>{r.destroy(),s(new Error("Request timed out"))}),r.on("error",s),r.end()})}function ge(e,n,t){return new Promise((a,s)=>{let r=(e.protocol==="https:"?T:x).request({hostname:e.hostname,port:e.port||(e.protocol==="https:"?443:80),path:e.pathname+e.search,method:"GET",headers:n,rejectUnauthorized:t,timeout:15e3},l=>{if(l.statusCode<200||l.statusCode>=300){l.resume(),s(new Error(`HTTP ${l.statusCode}`));return}let c=[];l.on("data",d=>c.push(d)),l.on("end",()=>{try{a(JSON.parse(Buffer.concat(c).toString("utf8")))}catch{s(new Error("Failed to parse JSON response"))}})});r.on("timeout",()=>{r.destroy(),s(new Error("Request timed out"))}),r.on("error",s),r.end()})}function K(e,n,t,a=5,s={}){return new Promise((i,r)=>{if(a===0){r(new Error("Too many redirects"));return}let c=(e.protocol==="https:"?T:x).request({hostname:e.hostname,port:e.port||(e.protocol==="https:"?443:80),path:e.pathname+e.search,method:"GET",headers:s,rejectUnauthorized:t,timeout:6e4},d=>{if(d.statusCode>=300&&d.statusCode<400&&d.headers.location){d.resume();let k=new URL(d.headers.location);if(k.hostname!==e.hostname||k.protocol!==e.protocol){r(new Error(`Redirect to untrusted destination blocked: ${k.protocol}//${k.hostname}`));return}K(k,n,t,a-1,s).then(i).catch(r);return}if(d.statusCode<200||d.statusCode>=300){d.resume(),r(new Error(`HTTP ${d.statusCode}`));return}let u=g.createWriteStream(n);d.pipe(u),u.on("finish",()=>u.close(()=>i())),u.on("error",r)});c.on("timeout",()=>{c.destroy(),r(new Error("Download timed out"))}),c.on("error",r),c.end()})}function V(e,n){let t=e.split(".").map(Number),a=n.split(".").map(Number);for(let s=0;s<3;s++){let i=(t[s]??0)-(a[s]??0);if(i!==0)return i<0?-1:1}return 0}var L=class{_onDidChange=new o.EventEmitter;onDidChangeMcpServerDefinitions=this._onDidChange.event;fireChanged(){this._onDidChange.fire()}async provideMcpServerDefinitions(n){re();let t=o.workspace.getConfiguration("opengrok-mcp"),a=t.get("username");if(!a)return[];let s=await S.get(`opengrok-password-${a}`),i=t.get("baseUrl")||"",r=t.get("verifySsl")??!1,l=t.get("proxy"),c={OPENGROK_BASE_URL:i,OPENGROK_USERNAME:a,OPENGROK_VERIFY_SSL:r?"true":"false"};if(s)try{let u=M.tmpdir(),k=`opengrok-cred-${E.randomBytes(16).toString("hex")}.tmp`,y=$.join(u,k),v=E.randomBytes(32),b=E.randomBytes(16),m=E.createCipheriv("aes-256-cbc",v,b),f=m.update(s,"utf8","base64");f+=m.final("base64");let h=`${b.toString("base64")}:${f}`;if(g.writeFileSync(y,h,{encoding:"utf8",mode:384}),process.platform==="win32")try{(0,H.execSync)(`icacls "${y}" /inheritance:r /grant:r "%username%:(F)" /Q`,{windowsHide:!0}),p("Windows ACL hardening applied to credential file")}catch(z){p(`Warning: Failed to apply Windows ACLs (file still protected by temp dir): ${z}`)}c.OPENGROK_PASSWORD_FILE=y,c.OPENGROK_PASSWORD_KEY=v.toString("base64"),p(`Credential file created (encrypted): ${y}`)}catch(u){p(`Warning: Failed to create credential file, falling back to env variable: ${u}`),c.OPENGROK_PASSWORD=s}l&&(c.HTTP_PROXY=l,c.HTTPS_PROXY=l);let d=await o.workspace.findFiles("**/compile_commands.json");return d.length>0&&(c.OPENGROK_LOCAL_COMPILE_DB_PATHS=d.map(u=>u.fsPath).join(",")),[{type:"stdio",label:"OpenGrok",command:process.execPath,args:[fe()],env:c,version:_()}]}};function fe(){let e=o.extensions.getExtension("IcyHot09.opengrok-mcp-server");return e?e.extensionUri.fsPath.replace(/\\/g,"/")+"/out/server/main.js":"out/server/main.js"}var C;function A(e){if(p("Opening Configuration Manager webview..."),C){C.reveal(o.ViewColumn.One);return}C=o.window.createWebviewPanel("opengrokConfig","OpenGrok Configuration",o.ViewColumn.One,{enableScripts:!0,retainContextWhenHidden:!0}),C.webview.html=he(e),C.onDidDispose(()=>{C=void 0}),C.webview.onDidReceiveMessage(async n=>{p(`Config webview message: ${n.type}`);try{switch(n.type){case"getConfig":await me(C);break;case"testConnection":await we(C,n.data);break;case"saveConfiguration":await ve(C,n.data);break}}catch(t){p(`Error handling webview message: ${t.message}`),C?.webview.postMessage({type:"error",message:t.message})}}),p("Configuration Manager webview created")}async function me(e){let n=o.workspace.getConfiguration("opengrok-mcp"),t=n.get("username")||"",a=n.get("baseUrl")||"",s=n.get("verifySsl")??!0,i=n.get("proxy")||"",r=!1;t&&(r=!!await S.get(`opengrok-password-${t}`)),e.webview.postMessage({type:"loadConfig",config:{baseUrl:a,username:t,verifySsl:s,proxy:i,hasPassword:r}})}async function we(e,n){let{baseUrl:t,username:a,password:s,verifySsl:i}=n;e.webview.postMessage({type:"testing",message:"Testing connection..."});try{let l=new URL(t).href.replace(/\/+$/,""),c=new URL(`${l}/api/v1/projects`),d=Buffer.from(`${a}:${s}`).toString("base64"),u={hostname:c.hostname,port:c.port||(c.protocol==="https:"?443:80),path:c.pathname+c.search,method:"GET",headers:{Authorization:`Basic ${d}`,Accept:"application/json"},rejectUnauthorized:i},k=c.protocol==="https:"?T:x;await new Promise((y,v)=>{let b=k.request(u,m=>{let f="";m.on("data",h=>{f+=h.toString()}),m.on("end",()=>{if(m.statusCode===401)v(new Error("Authentication failed (401). Check username/password."));else if(m.statusCode===403)v(new Error("Access denied (403). Check credentials or server permissions."));else if(!m.statusCode||m.statusCode>=400)v(new Error(`Server returned status ${m.statusCode} \u2014 is this an OpenGrok server?`));else try{let h=JSON.parse(f);Array.isArray(h)?y():v(new Error("Unexpected response \u2014 is this an OpenGrok server?"))}catch{v(new Error("Response is not JSON \u2014 is this an OpenGrok server?"))}})});b.on("error",m=>v(m)),b.setTimeout(1e4,()=>{b.destroy(),v(new Error("Connection timed out"))}),b.end()}),p("\u2713 Webview test connection successful"),e.webview.postMessage({type:"testSuccess",message:"\u2713 Connection successful!"})}catch(r){p(`\u2717 Webview test connection failed: ${r.message}`),e.webview.postMessage({type:"error",message:`\u2717 Connection failed: ${r.message}`})}}async function ve(e,n){let{baseUrl:t,username:a,password:s,proxy:i,verifySsl:r}=n,l=o.workspace.getConfiguration("opengrok-mcp"),c=l.get("username"),d=s;if(!d&&c&&(d=await S.get(`opengrok-password-${c}`)),!d){e.webview.postMessage({type:"error",message:"Password is required"});return}await l.update("baseUrl",t,o.ConfigurationTarget.Global),await l.update("username",a,o.ConfigurationTarget.Global),await l.update("verifySsl",r,o.ConfigurationTarget.Global),await l.update("proxy",i||void 0,o.ConfigurationTarget.Global),await S.store(`opengrok-password-${a}`,d),c&&c!==a&&await S.delete(`opengrok-password-${c}`),p(`Configuration saved for user: ${a}`),O("ready"),q(),e.webview.postMessage({type:"success",message:"Configuration saved successfully! Tools are actively refreshing."}),D()}function he(e){try{let n=$.join(e.extensionPath,"out","webview","configManager.html");if(g.existsSync(n))return g.readFileSync(n,"utf8")}catch(n){p(`Warning: Could not load external HTML: ${n}`)}return"<html><body><p>Configuration panel unavailable. Please reinstall the extension.</p></body></html>"}function Ce(){ae(),p("OpenGrok MCP extension deactivated")}0&&(module.exports={activate,deactivate});
1
+ "use strict";var Y=Object.create;var U=Object.defineProperty;var Q=Object.getOwnPropertyDescriptor;var X=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,Z=Object.prototype.hasOwnProperty;var ee=(e,n)=>{for(var t in n)U(e,t,{get:n[t],enumerable:!0})},B=(e,n,t,i)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of X(n))!Z.call(e,r)&&r!==t&&U(e,r,{get:()=>n[r],enumerable:!(i=Q(n,r))||i.enumerable});return e};var P=(e,n,t)=>(t=e!=null?Y(j(e)):{},B(n||!e||!e.__esModule?U(t,"default",{value:e,enumerable:!0}):t,e)),oe=e=>B(U({},"__esModule",{value:!0}),e);var be={};ee(be,{activate:()=>de,deactivate:()=>ke});module.exports=oe(be);var o=P(require("vscode")),g=P(require("fs")),x=P(require("http")),T=P(require("https")),$=P(require("os")),M=P(require("path")),E=P(require("crypto")),V=require("child_process"),ne=6e4,N="IcyHot09",W="opengrok-mcp-server",te="https://api.github.com",se=`https://github.com/${N}/${W}/releases`,re=1440*60*1e3,G,v,S,R,A=!1;function ie(){try{let e=$.tmpdir(),n=g.readdirSync(e),t=Date.now();for(let i of n)if(i.startsWith("opengrok-cred-")&&i.endsWith(".tmp")){let r=M.join(e,i);try{let c=g.statSync(r),s=t-c.mtimeMs;s>ne&&(g.unlinkSync(r),d(`Cleaned up stale credential file (${Math.round(s/1e3)}s old): ${r}`))}catch{}}}catch(e){d(`Warning: Failed to clean up stale credential files: ${e}`)}}function ae(){try{let e=$.tmpdir(),n=g.readdirSync(e);for(let t of n)if(t.startsWith("opengrok-cred-")&&t.endsWith(".tmp")){let i=M.join(e,t);try{g.unlinkSync(i),d(`Credential file cleaned up: ${i}`)}catch{}}}catch(e){d(`Warning: Failed to clean up credential files: ${e}`)}}function I(){return o.extensions.getExtension("IcyHot09.opengrok-mcp-server")?.packageJSON.version||"0.0.0"}function q(){R&&(R.fireChanged(),d("Notified VS Code of MCP server definition change."))}async function ce(e){let n=I(),t=e.globalState.get("extensionVersion");if(await e.globalState.update("extensionVersion",n),t&&t!==n){d(`Extension updated: ${t} \u2192 ${n}`);let i=await o.window.showInformationMessage(`OpenGrok MCP updated to v${n}! To use the latest features in Copilot Chat, please reload the window, then enable OpenGrok in the \u{1F527} Tools menu.`,"Reload Window","View Changelog","Later");i==="Reload Window"?o.commands.executeCommand("workbench.action.reloadWindow"):i==="View Changelog"&&o.env.openExternal(o.Uri.parse(`https://github.com/${N}/${W}/blob/main/CHANGELOG.md`)),q()}}async function H(e,n){let t=n?.manual??!1,r=o.workspace.getConfiguration("opengrok-mcp").get("verifySsl")??!0;if(!t){let c=e.globalState.get("lastUpdateCheck",0);if(Date.now()-c<re){d("Update check skipped \u2014 checked recently.");return}}try{d("Checking for extension updates...");let c=new URL(`${te}/repos/${N}/${W}/releases?per_page=5`),p=await fe(c,{"User-Agent":"OpenGrok-MCP-UpdateCheck/1.0",Accept:"application/vnd.github+json"},r);await e.globalState.update("lastUpdateCheck",Date.now());let a=null,l="";for(let f of p){if(f.prerelease||f.draft)continue;let C=(f.tag_name??"").replace(/^v/,"");!C||/beta|alpha|rc/i.test(C)||(!l||F(C,l)>0)&&(l=C,a=f)}if(!l){d("No stable version found in releases."),t&&o.window.showInformationMessage("OpenGrok MCP: No stable release found.");return}let m=I();if(d(`Update check: installed=${m}, latest=${l}`),F(l,m)<=0){d("Already on the latest version."),t&&o.window.showInformationMessage(`OpenGrok MCP: You are on the latest version (v${m}).`);return}if(!a){d("No stable version found in releases."),t&&o.window.showInformationMessage("OpenGrok MCP: No stable release found.");return}let h=a.assets?.find(f=>/\.vsix$/i.test(f.name??""))?.browser_download_url,u=`${se}/tag/v${l}`,y=["View Release Notes","Dismiss"];h&&y.unshift("Install Update");let w=await o.window.showInformationMessage(`OpenGrok MCP v${l} is available (you have v${m}).`,...y);if(w==="Install Update"&&h){let f=M.join($.tmpdir(),`opengrok-mcp-${l}.vsix`);await o.window.withProgress({location:o.ProgressLocation.Notification,title:`Downloading OpenGrok MCP v${l}...`,cancellable:!1},async()=>{await K(new URL(h),f,r)}),await o.commands.executeCommand("workbench.extensions.installExtension",o.Uri.file(f));try{g.unlinkSync(f)}catch{}await o.window.showInformationMessage(`OpenGrok MCP updated to v${l}! Reload to activate the new version.`,"Reload Window")==="Reload Window"&&o.commands.executeCommand("workbench.action.reloadWindow")}else w==="View Release Notes"&&o.env.openExternal(o.Uri.parse(u))}catch(c){let s=c instanceof Error?c.message:String(c);d(`Update check failed (non-fatal): ${s}`),t&&o.window.showWarningMessage(`OpenGrok MCP: Could not check for updates. ${s}`)}}async function de(e){S=e.secrets,G=o.window.createOutputChannel("OpenGrok MCP"),e.subscriptions.push(G),v=o.window.createStatusBarItem(o.StatusBarAlignment.Right,100),v.command="opengrok-mcp.showLogs",e.subscriptions.push(v);let n=I();d(`OpenGrok MCP v${n} activating...`),await ce(e),e.subscriptions.push(o.commands.registerCommand("opengrok-mcp.configure",le),o.commands.registerCommand("opengrok-mcp.configureUI",()=>_(e)),o.commands.registerCommand("opengrok-mcp.test",D),o.commands.registerCommand("opengrok-mcp.showLogs",()=>G.show()),o.commands.registerCommand("opengrok-mcp.statusMenu",pe),o.commands.registerCommand("opengrok-mcp.checkUpdate",()=>H(e,{manual:!0}))),H(e);let t=o.workspace.getConfiguration("opengrok-mcp"),i=t.get("username");o.lm&&o.lm.registerMcpServerDefinitionProvider?(R=new L,e.subscriptions.push(o.lm.registerMcpServerDefinitionProvider("opengrok-mcp-server",R)),d("Registered native MCP Server Definition Provider.")):d("Warning: This version of VS Code does not support native MCP Server Definition Providers. Copilot features may not work."),i?O("ready"):(A=!0,O("unconfigured"),await o.window.showInformationMessage("OpenGrok MCP: To enable Copilot codebase search, please configure your OpenGrok credentials.","Configure Now","Later")==="Configure Now"?_(e):t.get("hasPromptedConfig")||(_(e),await t.update("hasPromptedConfig",!0,o.ConfigurationTarget.Global))),d(`OpenGrok MCP v${n} activated`)}function d(e){G.appendLine(`[${new Date().toISOString()}] ${e}`)}function O(e){switch(e){case"ready":v.text="$(search) OpenGrok",v.tooltip="OpenGrok MCP: Ready - Click for options",v.backgroundColor=void 0,v.command="opengrok-mcp.statusMenu";break;case"error":v.text="$(warning) OpenGrok",v.tooltip="OpenGrok MCP: Error - Click for options",v.backgroundColor=new o.ThemeColor("statusBarItem.errorBackground"),v.command="opengrok-mcp.statusMenu";break;case"unconfigured":v.text="$(gear) OpenGrok",v.tooltip="OpenGrok MCP: Not configured - Click to setup",v.command="opengrok-mcp.configureUI";break}v.show()}async function pe(){let e=await o.window.showQuickPick([{label:"$(zap) Test Connection",detail:"Verify connection to the OpenGrok server",command:"opengrok-mcp.test"},{label:"$(settings-gear) Configuration Manager",detail:"Open visual configuration panel",command:"opengrok-mcp.configureUI"},{label:"$(gear) Quick Configure",detail:"Update credentials via input prompts",command:"opengrok-mcp.configure"},{label:"$(output) Show Server Logs",detail:"View diagnostic logs for learning and debugging",command:"opengrok-mcp.showLogs"},{label:"$(cloud-download) Check for Updates",detail:"Check for new extension versions on GitHub",command:"opengrok-mcp.checkUpdate"}],{placeHolder:"OpenGrok MCP Options"});e&&o.commands.executeCommand(e.command)}async function le(){let e=o.workspace.getConfiguration("opengrok-mcp"),n=e.get("baseUrl")||"",t=await o.window.showInputBox({prompt:"Enter OpenGrok server URL",value:n,ignoreFocusOut:!0,validateInput:s=>{try{return new URL(s),null}catch{return"Please enter a valid URL"}}});if(!t)return;let i=e.get("username")||"",r=await o.window.showInputBox({prompt:"Enter your OpenGrok username",value:i,ignoreFocusOut:!0});if(!r)return;let c=await o.window.showInputBox({prompt:"Enter your OpenGrok password",password:!0,ignoreFocusOut:!0});c&&(await e.update("baseUrl",t,o.ConfigurationTarget.Global),await e.update("username",r,o.ConfigurationTarget.Global),await S.store(`opengrok-password-${r}`,c),d(`Credentials saved for user: ${r}`),o.window.showInformationMessage("OpenGrok credentials saved securely! Copilot Chat can now search your codebase.","Test Connection").then(s=>{s==="Test Connection"&&D()}),O("ready"))}function ue(){o.window.showInformationMessage("Connection successful! First-time setup requires a window reload to enable OpenGrok tools in Copilot.","Reload Window").then(e=>{e==="Reload Window"&&o.commands.executeCommand("workbench.action.reloadWindow")})}async function D(){let e=o.workspace.getConfiguration("opengrok-mcp"),n=e.get("username"),t=e.get("baseUrl"),i=e.get("verifySsl")??!0;if(!n){o.window.showErrorMessage("OpenGrok: No username configured.","Configure Now").then(c=>{c==="Configure Now"&&o.commands.executeCommand("opengrok-mcp.configureUI")});return}let r=await S.get(`opengrok-password-${n}`);if(!r){o.window.showErrorMessage("OpenGrok: No password found. Please configure credentials.","Configure Now").then(c=>{c==="Configure Now"&&o.commands.executeCommand("opengrok-mcp.configureUI")});return}await o.window.withProgress({location:o.ProgressLocation.Notification,title:"Testing OpenGrok connection...",cancellable:!1},async()=>{try{let c=t||"",s=new URL(c),p=Buffer.from(`${n}:${r}`).toString("base64"),a=await ge(s,{Authorization:`Basic ${p}`,"User-Agent":"OpenGrok-MCP/2.0.0"},i);a>=200&&a<400?(d(`Connection test successful (HTTP ${a})`),o.window.showInformationMessage("\u2713 OpenGrok connection successful!"),O("ready"),A&&(ue(),A=!1)):a===401?(d("Authentication failed (401)"),o.window.showErrorMessage("\u2717 Authentication failed. Check your username and password."),O("error")):(d(`Unexpected status: ${a}`),o.window.showWarningMessage(`OpenGrok returned HTTP ${a}`))}catch(c){let s=c instanceof Error?c.message:String(c);d(`Connection test failed: ${s}`),s.includes("certificate")||s.includes("self-signed")||s.includes("CERT")||s.includes("SSL")?o.window.showErrorMessage("\u2717 SSL certificate error. If using a self-signed/internal CA, disable SSL verification in Settings.","Open Settings").then(p=>{p==="Open Settings"&&o.commands.executeCommand("workbench.action.openSettings","opengrok-mcp.verifySsl")}):o.window.showErrorMessage(`\u2717 Connection failed: ${s}`),O("error")}})}function ge(e,n,t){return new Promise((i,r)=>{let s=(e.protocol==="https:"?T:x).request({hostname:e.hostname,port:e.port||(e.protocol==="https:"?443:80),path:e.pathname+e.search,method:"GET",headers:n,rejectUnauthorized:t,timeout:15e3},p=>{p.resume(),i(p.statusCode??0)});s.on("timeout",()=>{s.destroy(),r(new Error("Request timed out"))}),s.on("error",r),s.end()})}function fe(e,n,t){return new Promise((i,r)=>{let s=(e.protocol==="https:"?T:x).request({hostname:e.hostname,port:e.port||(e.protocol==="https:"?443:80),path:e.pathname+e.search,method:"GET",headers:n,rejectUnauthorized:t,timeout:15e3},p=>{let a=p.statusCode??0;if(a<200||a>=300){p.resume(),r(new Error(`HTTP ${a}`));return}let l=[];p.on("data",m=>l.push(m)),p.on("end",()=>{try{i(JSON.parse(Buffer.concat(l).toString("utf8")))}catch{r(new Error("Failed to parse JSON response"))}})});s.on("timeout",()=>{s.destroy(),r(new Error("Request timed out"))}),s.on("error",r),s.end()})}function K(e,n,t,i=5,r={}){return new Promise((c,s)=>{if(i===0){s(new Error("Too many redirects"));return}let a=(e.protocol==="https:"?T:x).request({hostname:e.hostname,port:e.port||(e.protocol==="https:"?443:80),path:e.pathname+e.search,method:"GET",headers:r,rejectUnauthorized:t,timeout:6e4},l=>{let m=l.statusCode??0,b=l.headers.location;if(m>=300&&m<400&&b){l.resume();let u=new URL(b);if(u.hostname!==e.hostname||u.protocol!==e.protocol){s(new Error(`Redirect to untrusted destination blocked: ${u.protocol}//${u.hostname}`));return}K(u,n,t,i-1,r).then(c).catch(s);return}if(m<200||m>=300){l.resume(),s(new Error(`HTTP ${m}`));return}let h=g.createWriteStream(n);l.pipe(h),h.on("finish",()=>h.close(()=>c())),h.on("error",s)});a.on("timeout",()=>{a.destroy(),s(new Error("Download timed out"))}),a.on("error",s),a.end()})}function F(e,n){let t=e.split(".").map(Number),i=n.split(".").map(Number);for(let r=0;r<3;r++){let c=(t[r]??0)-(i[r]??0);if(c!==0)return c<0?-1:1}return 0}var L=class{_onDidChange=new o.EventEmitter;onDidChangeMcpServerDefinitions=this._onDidChange.event;fireChanged(){this._onDidChange.fire()}async provideMcpServerDefinitions(n){ie();let t=o.workspace.getConfiguration("opengrok-mcp"),i=t.get("username");if(!i)return[];let r=await S.get(`opengrok-password-${i}`),c=t.get("baseUrl")||"",s=t.get("verifySsl")??!1,p=t.get("proxy"),a={OPENGROK_BASE_URL:c,OPENGROK_USERNAME:i,OPENGROK_VERIFY_SSL:s?"true":"false"};if(r)try{let b=$.tmpdir(),h=`opengrok-cred-${E.randomBytes(16).toString("hex")}.tmp`,u=M.join(b,h),y=E.randomBytes(32),w=E.randomBytes(16),f=E.createCipheriv("aes-256-cbc",y,w),C=f.update(r,"utf8","base64");C+=f.final("base64");let z=`${w.toString("base64")}:${C}`;if(g.writeFileSync(u,z,{encoding:"utf8",mode:384}),process.platform==="win32")try{(0,V.execSync)(`icacls "${u}" /inheritance:r /grant:r "%username%:(F)" /Q`,{windowsHide:!0}),d("Windows ACL hardening applied to credential file")}catch(J){d(`Warning: Failed to apply Windows ACLs (file still protected by temp dir): ${J}`)}a.OPENGROK_PASSWORD_FILE=u,a.OPENGROK_PASSWORD_KEY=y.toString("base64"),d(`Credential file created (encrypted): ${u}`)}catch(b){d(`Warning: Failed to create credential file, falling back to env variable: ${b}`),a.OPENGROK_PASSWORD=r}p&&(a.HTTP_PROXY=p,a.HTTPS_PROXY=p);let l=await o.workspace.findFiles("**/compile_commands.json");return l.length>0&&(a.OPENGROK_LOCAL_COMPILE_DB_PATHS=l.map(b=>b.fsPath).join(",")),[new o.McpStdioServerDefinition("OpenGrok",process.execPath,[me()],a,I())]}};function me(){let e=o.extensions.getExtension("IcyHot09.opengrok-mcp-server");return e?e.extensionUri.fsPath.replace(/\\/g,"/")+"/out/server/main.js":"out/server/main.js"}var k;function _(e){if(d("Opening Configuration Manager webview..."),k){k.reveal(o.ViewColumn.One);return}k=o.window.createWebviewPanel("opengrokConfig","OpenGrok Configuration",o.ViewColumn.One,{enableScripts:!0,retainContextWhenHidden:!0}),k.webview.html=Ce(e),k.onDidDispose(()=>{k=void 0}),k.webview.onDidReceiveMessage(async n=>{if(d(`Config webview message: ${n.type}`),!!k)try{switch(n.type){case"getConfig":await we(k);break;case"testConnection":await ve(k,n.data);break;case"saveConfiguration":await he(k,n.data);break}}catch(t){let i=t instanceof Error?t.message:String(t);d(`Error handling webview message: ${i}`),k.webview.postMessage({type:"error",message:i})}}),d("Configuration Manager webview created")}async function we(e){let n=o.workspace.getConfiguration("opengrok-mcp"),t=n.get("username")||"",i=n.get("baseUrl")||"",r=n.get("verifySsl")??!0,c=n.get("proxy")||"",s=!1;t&&(s=!!await S.get(`opengrok-password-${t}`)),e.webview.postMessage({type:"loadConfig",config:{baseUrl:i,username:t,verifySsl:r,proxy:c,hasPassword:s}})}async function ve(e,n){let{baseUrl:t,username:i,password:r,verifySsl:c}=n;e.webview.postMessage({type:"testing",message:"Testing connection..."});try{let p=new URL(t).href.replace(/\/+$/,""),a=new URL(`${p}/api/v1/projects`),l=Buffer.from(`${i}:${r}`).toString("base64"),m={hostname:a.hostname,port:a.port||(a.protocol==="https:"?443:80),path:a.pathname+a.search,method:"GET",headers:{Authorization:`Basic ${l}`,Accept:"application/json"},rejectUnauthorized:c},b=a.protocol==="https:"?T:x;await new Promise((h,u)=>{let y=b.request(m,w=>{let f="";w.on("data",C=>{f+=C.toString()}),w.on("end",()=>{if(w.statusCode===401)u(new Error("Authentication failed (401). Check username/password."));else if(w.statusCode===403)u(new Error("Access denied (403). Check credentials or server permissions."));else if(!w.statusCode||w.statusCode>=400)u(new Error(`Server returned status ${w.statusCode} \u2014 is this an OpenGrok server?`));else try{let C=JSON.parse(f);Array.isArray(C)?h():u(new Error("Unexpected response \u2014 is this an OpenGrok server?"))}catch{u(new Error("Response is not JSON \u2014 is this an OpenGrok server?"))}})});y.on("error",w=>u(w)),y.setTimeout(1e4,()=>{y.destroy(),u(new Error("Connection timed out"))}),y.end()}),d("\u2713 Webview test connection successful"),e.webview.postMessage({type:"testSuccess",message:"\u2713 Connection successful!"})}catch(s){let p=s instanceof Error?s.message:String(s);d(`\u2717 Webview test connection failed: ${p}`),e.webview.postMessage({type:"error",message:`\u2717 Connection failed: ${p}`})}}async function he(e,n){let{baseUrl:t,username:i,password:r,proxy:c,verifySsl:s}=n,p=o.workspace.getConfiguration("opengrok-mcp"),a=p.get("username"),l=r;if(!l&&a&&(l=await S.get(`opengrok-password-${a}`)),!l){e.webview.postMessage({type:"error",message:"Password is required"});return}await p.update("baseUrl",t,o.ConfigurationTarget.Global),await p.update("username",i,o.ConfigurationTarget.Global),await p.update("verifySsl",s,o.ConfigurationTarget.Global),await p.update("proxy",c||void 0,o.ConfigurationTarget.Global),await S.store(`opengrok-password-${i}`,l),a&&a!==i&&await S.delete(`opengrok-password-${a}`),d(`Configuration saved for user: ${i}`),O("ready"),q(),e.webview.postMessage({type:"success",message:"Configuration saved successfully! Tools are actively refreshing."}),D()}function Ce(e){try{let n=M.join(e.extensionPath,"out","webview","configManager.html");if(g.existsSync(n))return g.readFileSync(n,"utf8")}catch(n){d(`Warning: Could not load external HTML: ${n}`)}return"<html><body><p>Configuration panel unavailable. Please reinstall the extension.</p></body></html>"}function ke(){ae(),d("OpenGrok MCP extension deactivated")}0&&(module.exports={activate,deactivate});