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 +67 -2
- package/CONTRIBUTING.md +24 -14
- package/README.md +39 -17
- package/evaluation.xml +88 -0
- package/out/extension.js +1 -1
- package/out/server/main.js +118 -97
- package/package.json +14 -5
- package/scripts/release.ps1 +2 -1
- package/server.json +21 -20
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
|
|
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 #
|
|
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 (`
|
|
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. **
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
+
4. **Add formatter** in `src/server/formatters.ts` for the Markdown output
|
|
142
152
|
|
|
143
|
-
|
|
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
|
|
7
|
+
**MCP server bridging OpenGrok search engine with AI for instant context across massive codebases**
|
|
8
8
|
|
|
9
9
|
[](https://marketplace.visualstudio.com/items?itemName=IcyHot09.opengrok-mcp-server) [](https://marketplace.visualstudio.com/items?itemName=IcyHot09.opengrok-mcp-server) [](https://www.npmjs.com/package/opengrok-mcp-server) [](https://registry.modelcontextprotocol.io) [](https://github.com/IcyHot09/opengrok-mcp-server/actions/workflows/ci.yml) [](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
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
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
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
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** β `
|
|
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
|
-
| `
|
|
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});
|