@kagan-sh/opensearch 0.1.0 → 0.3.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/CONTRIBUTING.md +2 -2
- package/README.md +56 -54
- package/SKILL.md +2 -2
- package/dist/config.d.ts +1 -0
- package/dist/config.js +16 -2
- package/dist/index.js +27 -9
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +99 -0
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +6 -2
- package/dist/schema.d.ts +7 -7
- package/dist/schema.js +1 -1
- package/dist/sources/web.d.ts +1 -1
- package/dist/sources/web.js +18 -26
- package/package.json +12 -1
package/CONTRIBUTING.md
CHANGED
|
@@ -70,7 +70,7 @@ This repo uses `semantic-release` on `main`.
|
|
|
70
70
|
|
|
71
71
|
- Prefer Conventional Commits for merge commits and direct commits to `main`
|
|
72
72
|
- Run `bun run release:dry-run` locally if you want to preview the next release
|
|
73
|
-
- npm publishing
|
|
73
|
+
- npm publishing now uses GitHub Actions trusted publishing via OIDC for `@kagan-sh/opensearch`
|
|
74
74
|
- The npm package must be linked to this repository under the `kagan_sh` publisher account before the first release
|
|
75
75
|
- Local `semantic-release --dry-run` still fails auth checks unless GitHub and npm credentials are present; the OIDC npm path is validated in GitHub Actions, not in a regular local shell
|
|
76
76
|
|
|
@@ -95,7 +95,7 @@ This repo uses `semantic-release` on `main`.
|
|
|
95
95
|
- CI workflow: `.github/workflows/ci.yml`
|
|
96
96
|
- Release workflow: `.github/workflows/release.yml`
|
|
97
97
|
- GitHub releases are automated with `semantic-release`
|
|
98
|
-
- npm publishing is
|
|
98
|
+
- npm publishing is configured to publish through OIDC without a long-lived npm token
|
|
99
99
|
|
|
100
100
|
## Pull requests
|
|
101
101
|
|
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/
|
|
4
|
-
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/
|
|
5
|
-
<img alt="OpenSearch — evidence-backed search for OpenCode" src="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/mark-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/mark-light.svg">
|
|
5
|
+
<img alt="OpenSearch — evidence-backed search for OpenCode" src="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/mark-dark.svg" width="100%">
|
|
6
6
|
</picture>
|
|
7
7
|
</p>
|
|
8
8
|
<p align="center">
|
|
9
|
+
<a href="https://www.npmjs.com/package/@kagan-sh/opensearch"><img src="https://img.shields.io/npm/v/%40kagan-sh%2Fopensearch?style=for-the-badge" alt="npm"></a>
|
|
9
10
|
<a href="https://github.com/kagan-sh/opensearch/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/kagan-sh/opensearch/ci.yml?style=for-the-badge&label=CI" alt="CI"></a>
|
|
10
11
|
<a href="https://kagan-sh.github.io/opensearch/"><img src="https://img.shields.io/badge/docs-github%20pages-181717?style=for-the-badge&logo=github" alt="Docs"></a>
|
|
11
12
|
<a href="https://opensource.org/license/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge" alt="License: MIT"></a>
|
|
@@ -14,77 +15,78 @@
|
|
|
14
15
|
<h3 align="center">
|
|
15
16
|
<a href="https://kagan-sh.github.io/opensearch/">Docs</a> ·
|
|
16
17
|
<a href="https://kagan-sh.github.io/opensearch/quickstart/">Quickstart</a> ·
|
|
17
|
-
<a href="https://kagan-sh.github.io/opensearch/
|
|
18
|
-
<a href="https://
|
|
18
|
+
<a href="https://kagan-sh.github.io/opensearch/guides/searxng/">SearXNG Setup</a> ·
|
|
19
|
+
<a href="https://kagan-sh.github.io/opensearch/reference/result-contract/">Reference</a> ·
|
|
20
|
+
<a href="https://raw.githubusercontent.com/kagan-sh/opensearch/main/SKILL.md">LLM Quick Reference</a>
|
|
19
21
|
</h3>
|
|
20
22
|
|
|
21
23
|
---
|
|
22
24
|
|
|
23
|
-
`@kagan-sh/opensearch` is an
|
|
25
|
+
`@kagan-sh/opensearch` is an evidence-backed search tool for AI coding agents. It searches the live web and public code in parallel, then returns structured JSON your agent can act on.
|
|
26
|
+
|
|
27
|
+
Works as an **OpenCode plugin** and as a **Claude Code MCP server**.
|
|
24
28
|
|
|
25
29
|
## Install
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
### Claude Code (MCP)
|
|
28
32
|
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
"$schema": "https://opencode.ai/config.json",
|
|
32
|
-
"plugin": ["@kagan-sh/opensearch"]
|
|
33
|
-
}
|
|
33
|
+
```bash
|
|
34
|
+
claude mcp add opensearch -- npx -y @kagan-sh/opensearch
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Full docs: **[kagan-sh.github.io/opensearch](https://kagan-sh.github.io/opensearch/)**.
|
|
37
|
+
To enable web search via SearXNG:
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- `OPENSEARCH_DEPTH=quick|thorough`
|
|
46
|
-
- `OPENSEARCH_SOURCE_SESSION=true|false`
|
|
47
|
-
- `OPENSEARCH_SOURCE_WEB=true|false`
|
|
48
|
-
- `OPENSEARCH_SOURCE_CODE=true|false`
|
|
49
|
-
- `OPENSEARCH_WEB_KEY=<exa_api_key>`
|
|
50
|
-
- `EXA_API_KEY=<exa_api_key>`
|
|
51
|
-
|
|
52
|
-
`OPENSEARCH_WEB_KEY` takes precedence over `EXA_API_KEY`. Web search is skipped when neither key is set.
|
|
53
|
-
|
|
54
|
-
## Tool
|
|
55
|
-
|
|
56
|
-
The plugin exposes a single tool: `opensearch`.
|
|
57
|
-
|
|
58
|
-
Arguments:
|
|
59
|
-
|
|
60
|
-
- `query: string`
|
|
61
|
-
- `sources?: ("session" | "web" | "code")[]`
|
|
62
|
-
- `depth?: "quick" | "thorough"`
|
|
39
|
+
```bash
|
|
40
|
+
claude mcp add opensearch \
|
|
41
|
+
-e OPENSEARCH_WEB_URL=http://localhost:8080 \
|
|
42
|
+
-- npx -y @kagan-sh/opensearch
|
|
43
|
+
```
|
|
63
44
|
|
|
64
|
-
|
|
45
|
+
Or add it to `.mcp.json` in your project root:
|
|
65
46
|
|
|
66
|
-
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"opensearch": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["-y", "@kagan-sh/opensearch"],
|
|
53
|
+
"env": {
|
|
54
|
+
"OPENSEARCH_WEB_URL": "http://localhost:8080"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
67
60
|
|
|
68
|
-
-
|
|
69
|
-
- `sources_queried`
|
|
70
|
-
- `sources_yielded`
|
|
71
|
-
- `sources_unavailable[]`
|
|
72
|
-
- `source_errors[]`
|
|
61
|
+
See the [Claude Code install guide](https://kagan-sh.github.io/opensearch/guides/claude-code/) for full details.
|
|
73
62
|
|
|
74
|
-
|
|
63
|
+
### OpenCode (plugin)
|
|
75
64
|
|
|
76
|
-
|
|
65
|
+
Add the plugin to `opencode.json`:
|
|
77
66
|
|
|
78
|
-
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"$schema": "https://opencode.ai/config.json",
|
|
70
|
+
"plugin": ["@kagan-sh/opensearch"]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
79
73
|
|
|
80
|
-
|
|
74
|
+
OpenCode installs npm plugins automatically at startup.
|
|
81
75
|
|
|
82
|
-
|
|
76
|
+
Full docs: **[kagan-sh.github.io/opensearch](https://kagan-sh.github.io/opensearch/)**. Web search uses a self-hosted `SearXNG` instance; setup lives in the docs.
|
|
83
77
|
|
|
84
|
-
##
|
|
78
|
+
## License
|
|
85
79
|
|
|
86
|
-
|
|
80
|
+
[MIT](LICENSE)
|
|
87
81
|
|
|
88
|
-
|
|
82
|
+
---
|
|
89
83
|
|
|
90
|
-
|
|
84
|
+
<p align="center">
|
|
85
|
+
<a href="https://www.star-history.com/#kagan-sh/opensearch&type=date">
|
|
86
|
+
<picture>
|
|
87
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=kagan-sh/opensearch&type=date&theme=dark" />
|
|
88
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=kagan-sh/opensearch&type=date" />
|
|
89
|
+
<img alt="Star History" src="https://api.star-history.com/svg?repos=kagan-sh/opensearch&type=date" width="600" />
|
|
90
|
+
</picture>
|
|
91
|
+
</a>
|
|
92
|
+
</p>
|
package/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: opensearch
|
|
3
3
|
description: Use OpenSearch for broad, evidence-backed investigation across session history, the web, and public code when a user asks to search, research, compare sources, or gather official docs and examples.
|
|
4
4
|
license: MIT
|
|
5
|
-
compatibility:
|
|
5
|
+
compatibility: Works as an OpenCode plugin or a Claude Code MCP server. The web source requires a SearXNG instance and OPENSEARCH_WEB_URL. In Claude Code, only web and code sources are available.
|
|
6
6
|
metadata:
|
|
7
7
|
version: 0.0.1
|
|
8
8
|
---
|
|
@@ -42,7 +42,7 @@ Use this skill when the task is primarily about finding, comparing, and synthesi
|
|
|
42
42
|
|
|
43
43
|
## Source Selection Guide
|
|
44
44
|
|
|
45
|
-
- `session` for prior decisions, earlier runs, and local conversation history
|
|
45
|
+
- `session` for prior decisions, earlier runs, and local conversation history (OpenCode only)
|
|
46
46
|
- `web` for official docs, changelogs, and current vendor guidance
|
|
47
47
|
- `code` for public implementation examples and real usage patterns
|
|
48
48
|
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Config, SourceId } from "./schema";
|
|
2
2
|
export declare function defaultConfig(): Config;
|
|
3
|
+
export declare function mcpDefaultConfig(): Config;
|
|
3
4
|
export declare function parsePluginConfig(input: unknown): Config | undefined;
|
|
4
5
|
export declare function isSourceAvailable(config: Config, source: SourceId): boolean;
|
|
5
6
|
export declare function resolveSources(config: Config, requested?: SourceId[]): {
|
package/dist/config.js
CHANGED
|
@@ -26,7 +26,7 @@ export function defaultConfig() {
|
|
|
26
26
|
session: parseBoolean("OPENSEARCH_SOURCE_SESSION", process.env.OPENSEARCH_SOURCE_SESSION, true),
|
|
27
27
|
web: {
|
|
28
28
|
enabled: parseBoolean("OPENSEARCH_SOURCE_WEB", process.env.OPENSEARCH_SOURCE_WEB, true),
|
|
29
|
-
|
|
29
|
+
url: process.env.OPENSEARCH_WEB_URL,
|
|
30
30
|
},
|
|
31
31
|
code: parseBoolean("OPENSEARCH_SOURCE_CODE", process.env.OPENSEARCH_SOURCE_CODE, true),
|
|
32
32
|
},
|
|
@@ -34,6 +34,20 @@ export function defaultConfig() {
|
|
|
34
34
|
synth: parseBoolean("OPENSEARCH_SYNTH", process.env.OPENSEARCH_SYNTH, true),
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
export function mcpDefaultConfig() {
|
|
38
|
+
return {
|
|
39
|
+
sources: {
|
|
40
|
+
session: false,
|
|
41
|
+
web: {
|
|
42
|
+
enabled: parseBoolean("OPENSEARCH_SOURCE_WEB", process.env.OPENSEARCH_SOURCE_WEB, true),
|
|
43
|
+
url: process.env.OPENSEARCH_WEB_URL,
|
|
44
|
+
},
|
|
45
|
+
code: parseBoolean("OPENSEARCH_SOURCE_CODE", process.env.OPENSEARCH_SOURCE_CODE, true),
|
|
46
|
+
},
|
|
47
|
+
depth: parseDepth(process.env.OPENSEARCH_DEPTH),
|
|
48
|
+
synth: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
37
51
|
export function parsePluginConfig(input) {
|
|
38
52
|
if (!input || typeof input !== "object" || !("opensearch" in input)) {
|
|
39
53
|
return undefined;
|
|
@@ -50,7 +64,7 @@ export function isSourceAvailable(config, source) {
|
|
|
50
64
|
if (source === "session")
|
|
51
65
|
return config.sources.session;
|
|
52
66
|
if (source === "web") {
|
|
53
|
-
return config.sources.web.enabled && Boolean(config.sources.web.
|
|
67
|
+
return config.sources.web.enabled && Boolean(config.sources.web.url);
|
|
54
68
|
}
|
|
55
69
|
return config.sources.code;
|
|
56
70
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,19 @@ import { noResultsResult, noSourcesResult, rawResultsResult, runSourceSearches,
|
|
|
4
4
|
import { DEPTHS, ResultSchema, SOURCE_IDS, } from "./schema";
|
|
5
5
|
import { synthesize } from "./synth";
|
|
6
6
|
const BRAND = "OpenSearch";
|
|
7
|
+
const TAGLINE = "evidence-backed search";
|
|
8
|
+
function sourceBadge(source) {
|
|
9
|
+
if (source === "session")
|
|
10
|
+
return "SESSION";
|
|
11
|
+
if (source === "web")
|
|
12
|
+
return "WEB";
|
|
13
|
+
return "CODE";
|
|
14
|
+
}
|
|
15
|
+
function sourceBadges(sources) {
|
|
16
|
+
if (sources.length === 0)
|
|
17
|
+
return ["OFFLINE"];
|
|
18
|
+
return sources.map(sourceBadge);
|
|
19
|
+
}
|
|
7
20
|
function serialize(result) {
|
|
8
21
|
return JSON.stringify(result, null, 2);
|
|
9
22
|
}
|
|
@@ -22,28 +35,32 @@ function describeSources(sources) {
|
|
|
22
35
|
return "session + web + code";
|
|
23
36
|
}
|
|
24
37
|
function runningTitle(sources) {
|
|
25
|
-
return `${BRAND}
|
|
38
|
+
return `${BRAND} // scanning ${describeSources(sources)}`;
|
|
26
39
|
}
|
|
27
40
|
function doneTitle(result) {
|
|
28
41
|
if (result.status === "no_sources")
|
|
29
|
-
return `${BRAND}
|
|
42
|
+
return `${BRAND} // unavailable`;
|
|
30
43
|
if (result.status === "no_results")
|
|
31
|
-
return `${BRAND}
|
|
44
|
+
return `${BRAND} // no matches`;
|
|
32
45
|
if (result.status === "raw") {
|
|
33
|
-
return `${BRAND}
|
|
46
|
+
return `${BRAND} // ${result.meta.sources_yielded} raw result${result.meta.sources_yielded === 1 ? "" : "s"}`;
|
|
34
47
|
}
|
|
35
48
|
if (result.status === "raw_fallback")
|
|
36
|
-
return `${BRAND}
|
|
37
|
-
return `${BRAND}
|
|
49
|
+
return `${BRAND} // evidence fallback`;
|
|
50
|
+
return `${BRAND} // ${result.meta.sources_yielded} result${result.meta.sources_yielded === 1 ? "" : "s"}`;
|
|
38
51
|
}
|
|
39
52
|
function toolMetadata(input) {
|
|
40
53
|
return {
|
|
41
54
|
brand: BRAND,
|
|
55
|
+
brand_tagline: TAGLINE,
|
|
56
|
+
brand_origin: "@kagan-sh/opensearch",
|
|
57
|
+
brand_surface: "plugin",
|
|
42
58
|
phase: input.phase,
|
|
43
59
|
query: previewQuery(input.query),
|
|
44
60
|
depth: input.depth,
|
|
45
61
|
sources: input.sources,
|
|
46
62
|
source_summary: describeSources(input.sources),
|
|
63
|
+
source_badges: sourceBadges(input.sources),
|
|
47
64
|
...(input.result
|
|
48
65
|
? {
|
|
49
66
|
status: input.result.status,
|
|
@@ -84,7 +101,7 @@ export const OpensearchPlugin = async (ctx) => {
|
|
|
84
101
|
if (input.toolID !== "opensearch")
|
|
85
102
|
return;
|
|
86
103
|
output.description =
|
|
87
|
-
"OpenSearch
|
|
104
|
+
"OpenSearch // evidence-backed search across session history, SearXNG web search, and public code. Use it for broad investigation, comparison, docs gathering, and cross-source research.";
|
|
88
105
|
},
|
|
89
106
|
"tool.execute.after": async (input, output) => {
|
|
90
107
|
if (input.tool !== "opensearch")
|
|
@@ -110,7 +127,7 @@ export const OpensearchPlugin = async (ctx) => {
|
|
|
110
127
|
},
|
|
111
128
|
tool: {
|
|
112
129
|
opensearch: tool({
|
|
113
|
-
description: "
|
|
130
|
+
description: "OpenSearch // evidence-backed search. Queries session history, SearXNG web search, and public code in parallel, then returns a structured answer with explicit status and source metadata.",
|
|
114
131
|
args: {
|
|
115
132
|
query: tool.schema.string().describe("What to search for"),
|
|
116
133
|
sources: tool.schema
|
|
@@ -185,7 +202,7 @@ export const OpensearchPlugin = async (ctx) => {
|
|
|
185
202
|
}
|
|
186
203
|
if (cfg.synth) {
|
|
187
204
|
context.metadata({
|
|
188
|
-
title: `${BRAND}
|
|
205
|
+
title: `${BRAND} // assembling evidence`,
|
|
189
206
|
metadata: {
|
|
190
207
|
...toolMetadata({
|
|
191
208
|
phase: "synthesizing",
|
|
@@ -194,6 +211,7 @@ export const OpensearchPlugin = async (ctx) => {
|
|
|
194
211
|
sources: resolved.sources,
|
|
195
212
|
}),
|
|
196
213
|
raw_results: search.raw.length,
|
|
214
|
+
status_note: "assembling evidence",
|
|
197
215
|
},
|
|
198
216
|
});
|
|
199
217
|
try {
|
package/dist/mcp.d.ts
ADDED
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { mcpDefaultConfig, resolveSources } from "./config";
|
|
6
|
+
import { noResultsResult, noSourcesResult, rawResultsResult, runSourceSearches, } from "./orchestrator";
|
|
7
|
+
import { SOURCE_IDS } from "./schema";
|
|
8
|
+
function serialize(value) {
|
|
9
|
+
return JSON.stringify(value, null, 2);
|
|
10
|
+
}
|
|
11
|
+
function parseSourceIds(raw) {
|
|
12
|
+
if (!Array.isArray(raw))
|
|
13
|
+
return undefined;
|
|
14
|
+
return raw.filter((s) => typeof s === "string" && SOURCE_IDS.includes(s));
|
|
15
|
+
}
|
|
16
|
+
const config = mcpDefaultConfig();
|
|
17
|
+
const server = new Server({ name: "opensearch", version: "0.0.1" }, { capabilities: { tools: {} } });
|
|
18
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
19
|
+
tools: [
|
|
20
|
+
{
|
|
21
|
+
name: "opensearch",
|
|
22
|
+
description: "Evidence-backed search across SearXNG web and public code. Returns structured JSON with sources, relevance scores, and follow-up suggestions.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
query: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "What to search for",
|
|
29
|
+
},
|
|
30
|
+
sources: {
|
|
31
|
+
type: "array",
|
|
32
|
+
items: { type: "string", enum: ["web", "code"] },
|
|
33
|
+
description: "Sources to query. Defaults to all enabled.",
|
|
34
|
+
},
|
|
35
|
+
depth: {
|
|
36
|
+
type: "string",
|
|
37
|
+
enum: ["quick", "thorough"],
|
|
38
|
+
description: "Search depth. Default: quick",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ["query"],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
}));
|
|
46
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
47
|
+
if (request.params.name !== "opensearch") {
|
|
48
|
+
return {
|
|
49
|
+
isError: true,
|
|
50
|
+
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const args = request.params.arguments ?? {};
|
|
54
|
+
const query = typeof args.query === "string" ? args.query : "";
|
|
55
|
+
const depth = args.depth === "thorough" ? "thorough" : config.depth;
|
|
56
|
+
const start = Date.now();
|
|
57
|
+
const requested = parseSourceIds(args.sources);
|
|
58
|
+
const resolved = resolveSources(config, requested);
|
|
59
|
+
if (resolved.sources.length === 0) {
|
|
60
|
+
const result = noSourcesResult({
|
|
61
|
+
query,
|
|
62
|
+
start,
|
|
63
|
+
requested: resolved.requested,
|
|
64
|
+
unavailable: resolved.unavailable,
|
|
65
|
+
});
|
|
66
|
+
return { content: [{ type: "text", text: serialize(result) }] };
|
|
67
|
+
}
|
|
68
|
+
const search = await runSourceSearches({
|
|
69
|
+
directory: process.cwd(),
|
|
70
|
+
config,
|
|
71
|
+
query,
|
|
72
|
+
depth,
|
|
73
|
+
sources: resolved.sources,
|
|
74
|
+
});
|
|
75
|
+
if (search.raw.length === 0) {
|
|
76
|
+
const result = noResultsResult({
|
|
77
|
+
query,
|
|
78
|
+
start,
|
|
79
|
+
requested: resolved.requested,
|
|
80
|
+
queried: resolved.sources,
|
|
81
|
+
unavailable: resolved.unavailable,
|
|
82
|
+
sourceErrors: search.sourceErrors,
|
|
83
|
+
});
|
|
84
|
+
return { content: [{ type: "text", text: serialize(result) }] };
|
|
85
|
+
}
|
|
86
|
+
const result = rawResultsResult({
|
|
87
|
+
query,
|
|
88
|
+
start,
|
|
89
|
+
requested: resolved.requested,
|
|
90
|
+
queried: resolved.sources,
|
|
91
|
+
unavailable: resolved.unavailable,
|
|
92
|
+
sourceErrors: search.sourceErrors,
|
|
93
|
+
raw: search.raw,
|
|
94
|
+
status: "raw",
|
|
95
|
+
});
|
|
96
|
+
return { content: [{ type: "text", text: serialize(result) }] };
|
|
97
|
+
});
|
|
98
|
+
const transport = new StdioServerTransport();
|
|
99
|
+
await server.connect(transport);
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { createOpencodeClient } from "@opencode-ai/sdk";
|
|
|
2
2
|
import type { Config, RawResult, Result, Source, SourceError, SourceId, Synthesis } from "./schema";
|
|
3
3
|
export declare function normalize(raw: RawResult): Source;
|
|
4
4
|
export declare function runSourceSearches(input: {
|
|
5
|
-
client
|
|
5
|
+
client?: ReturnType<typeof createOpencodeClient>;
|
|
6
6
|
directory: string;
|
|
7
7
|
config: Config;
|
|
8
8
|
query: string;
|
package/dist/orchestrator.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { searchCode } from "./sources/code";
|
|
2
2
|
import { searchSessions } from "./sources/session";
|
|
3
|
+
import { failure } from "./sources/shared";
|
|
3
4
|
import { searchWeb } from "./sources/web";
|
|
4
5
|
function clampUnit(value) {
|
|
5
6
|
return Math.max(0, Math.min(1, value));
|
|
@@ -40,10 +41,13 @@ function createResult(input) {
|
|
|
40
41
|
export async function runSourceSearches(input) {
|
|
41
42
|
const outcomes = await Promise.all(input.sources.map((source) => {
|
|
42
43
|
if (source === "session") {
|
|
44
|
+
if (!input.client) {
|
|
45
|
+
return failure("session", "unavailable", "Session search requires the OpenCode runtime.");
|
|
46
|
+
}
|
|
43
47
|
return searchSessions(input.client, input.directory, input.query, input.depth);
|
|
44
48
|
}
|
|
45
49
|
if (source === "web") {
|
|
46
|
-
return searchWeb(input.query, input.config.sources.web.
|
|
50
|
+
return searchWeb(input.query, input.config.sources.web.url, input.depth);
|
|
47
51
|
}
|
|
48
52
|
return searchCode(input.query, input.depth);
|
|
49
53
|
}));
|
|
@@ -61,7 +65,7 @@ export function noSourcesResult(input) {
|
|
|
61
65
|
sources: [],
|
|
62
66
|
followups: [
|
|
63
67
|
"Enable at least one search source in opensearch config",
|
|
64
|
-
"Set
|
|
68
|
+
"Set OPENSEARCH_WEB_URL to enable web search",
|
|
65
69
|
],
|
|
66
70
|
meta: resultMeta({
|
|
67
71
|
query: input.query,
|
package/dist/schema.d.ts
CHANGED
|
@@ -346,27 +346,27 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
346
346
|
session: z.ZodBoolean;
|
|
347
347
|
web: z.ZodObject<{
|
|
348
348
|
enabled: z.ZodBoolean;
|
|
349
|
-
|
|
349
|
+
url: z.ZodOptional<z.ZodString>;
|
|
350
350
|
}, "strict", z.ZodTypeAny, {
|
|
351
351
|
enabled: boolean;
|
|
352
|
-
|
|
352
|
+
url?: string | undefined;
|
|
353
353
|
}, {
|
|
354
354
|
enabled: boolean;
|
|
355
|
-
|
|
355
|
+
url?: string | undefined;
|
|
356
356
|
}>;
|
|
357
357
|
code: z.ZodBoolean;
|
|
358
358
|
}, "strict", z.ZodTypeAny, {
|
|
359
359
|
session: boolean;
|
|
360
360
|
web: {
|
|
361
361
|
enabled: boolean;
|
|
362
|
-
|
|
362
|
+
url?: string | undefined;
|
|
363
363
|
};
|
|
364
364
|
code: boolean;
|
|
365
365
|
}, {
|
|
366
366
|
session: boolean;
|
|
367
367
|
web: {
|
|
368
368
|
enabled: boolean;
|
|
369
|
-
|
|
369
|
+
url?: string | undefined;
|
|
370
370
|
};
|
|
371
371
|
code: boolean;
|
|
372
372
|
}>;
|
|
@@ -377,7 +377,7 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
377
377
|
session: boolean;
|
|
378
378
|
web: {
|
|
379
379
|
enabled: boolean;
|
|
380
|
-
|
|
380
|
+
url?: string | undefined;
|
|
381
381
|
};
|
|
382
382
|
code: boolean;
|
|
383
383
|
};
|
|
@@ -388,7 +388,7 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
388
388
|
session: boolean;
|
|
389
389
|
web: {
|
|
390
390
|
enabled: boolean;
|
|
391
|
-
|
|
391
|
+
url?: string | undefined;
|
|
392
392
|
};
|
|
393
393
|
code: boolean;
|
|
394
394
|
};
|
package/dist/schema.js
CHANGED
package/dist/sources/web.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Depth } from "../schema";
|
|
2
2
|
import { type SourceSearchOutcome } from "./shared";
|
|
3
|
-
export declare function searchWeb(query: string,
|
|
3
|
+
export declare function searchWeb(query: string, baseUrl: string | undefined, depth: Depth): Promise<SourceSearchOutcome>;
|
package/dist/sources/web.js
CHANGED
|
@@ -1,46 +1,38 @@
|
|
|
1
1
|
import { failure, messageFromError } from "./shared";
|
|
2
|
-
export async function searchWeb(query,
|
|
2
|
+
export async function searchWeb(query, baseUrl, depth) {
|
|
3
3
|
const source = "web";
|
|
4
|
-
if (!
|
|
5
|
-
return failure(source, "unavailable", "Web source requires
|
|
4
|
+
if (!baseUrl) {
|
|
5
|
+
return failure(source, "unavailable", "Web source requires OPENSEARCH_WEB_URL.");
|
|
6
6
|
}
|
|
7
7
|
try {
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const url = new URL("search", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
9
|
+
url.searchParams.set("q", query);
|
|
10
|
+
url.searchParams.set("format", "json");
|
|
11
|
+
const res = await fetch(url, {
|
|
10
12
|
headers: {
|
|
11
|
-
"
|
|
12
|
-
"Content-Type": "application/json",
|
|
13
|
+
Accept: "application/json",
|
|
13
14
|
},
|
|
14
|
-
body: JSON.stringify({
|
|
15
|
-
query,
|
|
16
|
-
numResults: depth === "quick" ? 5 : 10,
|
|
17
|
-
type: "auto",
|
|
18
|
-
contents: {
|
|
19
|
-
text: {
|
|
20
|
-
maxCharacters: 1000,
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
}),
|
|
24
15
|
});
|
|
25
16
|
if (!res.ok) {
|
|
26
|
-
return failure(source, "request_failed",
|
|
17
|
+
return failure(source, "request_failed", res.status === 403
|
|
18
|
+
? "SearXNG search failed with status 403. Enable JSON output on the instance."
|
|
19
|
+
: `SearXNG search failed with status ${res.status}.`);
|
|
27
20
|
}
|
|
28
21
|
const body = (await res.json());
|
|
29
22
|
if (!Array.isArray(body.results)) {
|
|
30
|
-
return failure(source, "invalid_response", "
|
|
23
|
+
return failure(source, "invalid_response", "SearXNG search returned an invalid payload.");
|
|
31
24
|
}
|
|
32
|
-
const
|
|
25
|
+
const limit = depth === "quick" ? 5 : 10;
|
|
26
|
+
const list = body.results.slice(0, limit);
|
|
33
27
|
return {
|
|
34
28
|
source,
|
|
35
29
|
results: list.map((item, i) => ({
|
|
36
|
-
id:
|
|
30
|
+
id: `web-${i}`,
|
|
37
31
|
type: source,
|
|
38
32
|
title: item.title ?? item.url ?? "Untitled",
|
|
39
|
-
snippet: (item.
|
|
33
|
+
snippet: (item.content ?? item.url ?? "").slice(0, 700),
|
|
40
34
|
url: item.url,
|
|
41
|
-
relevance:
|
|
42
|
-
? Math.min(1, Math.max(0, item.score))
|
|
43
|
-
: 0.5,
|
|
35
|
+
relevance: Math.max(0.1, 1 - i / Math.max(1, list.length)),
|
|
44
36
|
timestamp: item.publishedDate
|
|
45
37
|
? Date.parse(item.publishedDate) || undefined
|
|
46
38
|
: undefined,
|
|
@@ -48,6 +40,6 @@ export async function searchWeb(query, key, depth) {
|
|
|
48
40
|
};
|
|
49
41
|
}
|
|
50
42
|
catch (error) {
|
|
51
|
-
return failure(source, "request_failed", `
|
|
43
|
+
return failure(source, "request_failed", `SearXNG search failed: ${messageFromError(error)}`);
|
|
52
44
|
}
|
|
53
45
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kagan-sh/opensearch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "OpenCode plugin that combines session, web, and code search",
|
|
5
|
+
"license": "MIT",
|
|
5
6
|
"homepage": "https://kagan-sh.github.io/opensearch/",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
@@ -13,11 +14,19 @@
|
|
|
13
14
|
"type": "module",
|
|
14
15
|
"main": "dist/index.js",
|
|
15
16
|
"types": "dist/index.d.ts",
|
|
17
|
+
"bin": {
|
|
18
|
+
"opensearch-mcp": "./dist/mcp.js"
|
|
19
|
+
},
|
|
16
20
|
"exports": {
|
|
17
21
|
".": {
|
|
18
22
|
"types": "./dist/index.d.ts",
|
|
19
23
|
"import": "./dist/index.js",
|
|
20
24
|
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./mcp": {
|
|
27
|
+
"types": "./dist/mcp.d.ts",
|
|
28
|
+
"import": "./dist/mcp.js",
|
|
29
|
+
"default": "./dist/mcp.js"
|
|
21
30
|
}
|
|
22
31
|
},
|
|
23
32
|
"files": [
|
|
@@ -28,6 +37,7 @@
|
|
|
28
37
|
"LICENSE"
|
|
29
38
|
],
|
|
30
39
|
"scripts": {
|
|
40
|
+
"mcp": "bun run src/mcp.ts",
|
|
31
41
|
"build": "bun tsc",
|
|
32
42
|
"typecheck": "bun tsc --noEmit",
|
|
33
43
|
"test": "vitest run",
|
|
@@ -39,6 +49,7 @@
|
|
|
39
49
|
"release:dry-run": "semantic-release --dry-run"
|
|
40
50
|
},
|
|
41
51
|
"dependencies": {
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
42
53
|
"@opencode-ai/plugin": "1.2.27",
|
|
43
54
|
"@opencode-ai/sdk": "1.2.27",
|
|
44
55
|
"zod": "^3.24.0",
|