@kagan-sh/opensearch 0.2.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/README.md CHANGED
@@ -22,10 +22,46 @@
22
22
 
23
23
  ---
24
24
 
25
- `@kagan-sh/opensearch` is an OpenCode plugin for broad investigation. It searches session history, the live web, and public code in parallel, then returns a structured evidence-backed response your agent can act on.
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**.
26
28
 
27
29
  ## Install
28
30
 
31
+ ### Claude Code (MCP)
32
+
33
+ ```bash
34
+ claude mcp add opensearch -- npx -y @kagan-sh/opensearch
35
+ ```
36
+
37
+ To enable web search via SearXNG:
38
+
39
+ ```bash
40
+ claude mcp add opensearch \
41
+ -e OPENSEARCH_WEB_URL=http://localhost:8080 \
42
+ -- npx -y @kagan-sh/opensearch
43
+ ```
44
+
45
+ Or add it to `.mcp.json` in your project root:
46
+
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
+ ```
60
+
61
+ See the [Claude Code install guide](https://kagan-sh.github.io/opensearch/guides/claude-code/) for full details.
62
+
63
+ ### OpenCode (plugin)
64
+
29
65
  Add the plugin to `opencode.json`:
30
66
 
31
67
  ```json
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: Requires OpenCode with @kagan-sh/opensearch installed. The current web provider is SearXNG, so web results need OPENSEARCH_WEB_URL.
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
@@ -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;
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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);
@@ -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: ReturnType<typeof createOpencodeClient>;
5
+ client?: ReturnType<typeof createOpencodeClient>;
6
6
  directory: string;
7
7
  config: Config;
8
8
  query: string;
@@ -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,6 +41,9 @@ 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") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kagan-sh/opensearch",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "OpenCode plugin that combines session, web, and code search",
5
5
  "license": "MIT",
6
6
  "homepage": "https://kagan-sh.github.io/opensearch/",
@@ -14,11 +14,19 @@
14
14
  "type": "module",
15
15
  "main": "dist/index.js",
16
16
  "types": "dist/index.d.ts",
17
+ "bin": {
18
+ "opensearch-mcp": "./dist/mcp.js"
19
+ },
17
20
  "exports": {
18
21
  ".": {
19
22
  "types": "./dist/index.d.ts",
20
23
  "import": "./dist/index.js",
21
24
  "default": "./dist/index.js"
25
+ },
26
+ "./mcp": {
27
+ "types": "./dist/mcp.d.ts",
28
+ "import": "./dist/mcp.js",
29
+ "default": "./dist/mcp.js"
22
30
  }
23
31
  },
24
32
  "files": [
@@ -29,6 +37,7 @@
29
37
  "LICENSE"
30
38
  ],
31
39
  "scripts": {
40
+ "mcp": "bun run src/mcp.ts",
32
41
  "build": "bun tsc",
33
42
  "typecheck": "bun tsc --noEmit",
34
43
  "test": "vitest run",
@@ -40,6 +49,7 @@
40
49
  "release:dry-run": "semantic-release --dry-run"
41
50
  },
42
51
  "dependencies": {
52
+ "@modelcontextprotocol/sdk": "^1.27.1",
43
53
  "@opencode-ai/plugin": "1.2.27",
44
54
  "@opencode-ai/sdk": "1.2.27",
45
55
  "zod": "^3.24.0",