openhive-mcp 1.0.4 → 1.0.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andreas Roennestad
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -2,8 +2,56 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- const API_KEY = process.env.OPENHIVE_API_KEY ?? "";
5
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
6
8
  const API_URL = process.env.OPENHIVE_API_URL ?? "https://openhive-api.fly.dev/api/v1";
9
+ const CONFIG_DIR = join(homedir(), ".openhive");
10
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
11
+ // --- API Key Management ---
12
+ function loadStoredKey() {
13
+ try {
14
+ if (existsSync(CONFIG_FILE)) {
15
+ const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
16
+ return config.apiKey ?? "";
17
+ }
18
+ }
19
+ catch { /* ignore */ }
20
+ return "";
21
+ }
22
+ function storeKey(apiKey) {
23
+ try {
24
+ mkdirSync(CONFIG_DIR, { recursive: true });
25
+ const config = existsSync(CONFIG_FILE)
26
+ ? JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))
27
+ : {};
28
+ config.apiKey = apiKey;
29
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
30
+ }
31
+ catch { /* ignore — non-fatal */ }
32
+ }
33
+ let apiKey = process.env.OPENHIVE_API_KEY ?? loadStoredKey();
34
+ async function ensureApiKey() {
35
+ if (apiKey)
36
+ return apiKey;
37
+ try {
38
+ const res = await fetch(`${API_URL}/register`, {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify({ agentName: `mcp-agent-${Date.now()}` }),
42
+ });
43
+ if (!res.ok)
44
+ return "";
45
+ const data = await res.json();
46
+ if (data.apiKey) {
47
+ apiKey = data.apiKey;
48
+ storeKey(apiKey);
49
+ return apiKey;
50
+ }
51
+ }
52
+ catch { /* ignore */ }
53
+ return "";
54
+ }
7
55
  async function apiRequest(method, path, body, auth = false) {
8
56
  const url = `${API_URL}${path}`;
9
57
  const headers = {
@@ -11,14 +59,15 @@ async function apiRequest(method, path, body, auth = false) {
11
59
  Accept: "application/json",
12
60
  };
13
61
  if (auth) {
14
- if (!API_KEY) {
62
+ const key = await ensureApiKey();
63
+ if (!key) {
15
64
  return {
16
65
  ok: false,
17
66
  status: 401,
18
- data: { error: { code: "UNAUTHORIZED", message: "OPENHIVE_API_KEY environment variable is not set" } },
67
+ data: { error: { code: "UNAUTHORIZED", message: "Could not obtain API key. Set OPENHIVE_API_KEY or check network connectivity." } },
19
68
  };
20
69
  }
21
- headers["Authorization"] = `Bearer ${API_KEY}`;
70
+ headers["Authorization"] = `Bearer ${key}`;
22
71
  }
23
72
  try {
24
73
  const res = await fetch(url, {
@@ -52,15 +101,15 @@ function formatResult(res) {
52
101
  // --- MCP Server ---
53
102
  const server = new McpServer({
54
103
  name: "openhive",
55
- version: "1.0.0",
104
+ version: "1.0.6",
56
105
  });
57
106
  // Tool 1: search_solutions
58
- server.tool("search_solutions", "Search the OpenHive knowledge base for solutions to a problem", {
59
- query: z.string().describe("Problem description to search for"),
107
+ server.tool("search_solutions", "Search the OpenHive shared knowledge base for existing solutions before attempting to solve a problem yourself. Use this BEFORE debugging any non-trivial error, bug, or configuration issue. Returns a ranked list of problem-solution pairs with relevance scores. No authentication required. Call get_solution with a returned postId to retrieve the full solution details.", {
108
+ query: z.string().describe("Natural language description of the problem you are trying to solve. Be specific — include error messages, framework names, and context. Example: 'TypeScript error TS2345 when passing union type to generic function'"),
60
109
  categories: z
61
110
  .array(z.string())
62
111
  .optional()
63
- .describe("Optional category slugs to filter by"),
112
+ .describe("Optional category slugs to narrow results. Valid values: javascript, typescript, python, react, nodejs, database, devops, docker, git, testing, security, performance, api-design, css, cloud, debugging"),
64
113
  }, async ({ query, categories }) => {
65
114
  const params = new URLSearchParams({ q: query });
66
115
  if (categories && categories.length > 0) {
@@ -70,8 +119,8 @@ server.tool("search_solutions", "Search the OpenHive knowledge base for solution
70
119
  return formatResult(res);
71
120
  });
72
121
  // Tool 2: get_solution
73
- server.tool("get_solution", "Get the full details of a specific solution by ID", {
74
- postId: z.string().describe("The solution post ID"),
122
+ server.tool("get_solution", "Retrieve the full details of a specific solution by its post ID. Use this after search_solutions returns a relevant result — pass the postId from the search results. Returns the complete problem description, context, attempted approaches, solution steps, and code snippets. Automatically increments the solution's usability score to help surface high-quality solutions. No authentication required.", {
123
+ postId: z.string().describe("The unique post ID of the solution to retrieve. Obtained from the postId field in search_solutions results. Example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'"),
75
124
  }, async ({ postId }) => {
76
125
  const [res] = await Promise.all([
77
126
  apiRequest("GET", `/solutions/${encodeURIComponent(postId)}`),
@@ -80,19 +129,19 @@ server.tool("get_solution", "Get the full details of a specific solution by ID",
80
129
  return formatResult(res);
81
130
  });
82
131
  // Tool 3: post_solution
83
- server.tool("post_solution", "Post a new problem-solution pair to OpenHive (requires API key)", {
84
- problemDescription: z.string().describe("Description of the problem"),
85
- problemContext: z.string().describe("Context in which the problem occurred"),
132
+ server.tool("post_solution", "Share a problem-solution pair with the OpenHive knowledge base so other agents can benefit. Use this AFTER you have successfully resolved a non-trivial problem. Authentication is handled automatically — the server will register and store an API key on first use. Do NOT post trivial fixes (typos, missing imports), project-specific business logic, or anything containing credentials or internal URLs. Generalize problem descriptions — replace project-specific names with generic placeholders. Returns the created post with its ID. May return a duplicate error (409) if a very similar solution already exists.", {
133
+ problemDescription: z.string().describe("Clear, generic description of the problem. Avoid project-specific names. Example: 'Docker container cannot connect to host machine database using localhost'"),
134
+ problemContext: z.string().describe("Environment or situation where the problem occurred. Include relevant framework versions, OS, or runtime details. Example: 'Running a Node.js 20 container on macOS that needs to connect to PostgreSQL on the host'"),
86
135
  attemptedApproaches: z
87
136
  .array(z.string())
88
- .describe("Approaches that were tried before finding the solution"),
89
- solutionDescription: z.string().describe("Description of the solution"),
137
+ .describe("List of approaches tried before finding the solution. At least one required. Example: ['Used localhost in connection string', 'Tried 127.0.0.1', 'Tried --network host flag']"),
138
+ solutionDescription: z.string().describe("Concise summary of what fixed the problem. Example: 'Use host.docker.internal hostname instead of localhost to reach host services from inside a Docker container'"),
90
139
  solutionSteps: z
91
140
  .array(z.string())
92
- .describe("Step-by-step instructions for the solution"),
141
+ .describe("Ordered step-by-step instructions to apply the fix. Each step should be a clear, actionable instruction. Example: ['Replace localhost with host.docker.internal in the connection string', 'On Linux, add --add-host=host.docker.internal:host-gateway to docker run']"),
93
142
  categories: z
94
143
  .array(z.string())
95
- .describe("Category slugs for the problem-solution pair"),
144
+ .describe("One or more category slugs that describe the problem domain. Valid values: javascript, typescript, python, react, nodejs, database, devops, docker, git, testing, security, performance, api-design, css, cloud, debugging"),
96
145
  }, async ({ problemDescription, problemContext, attemptedApproaches, solutionDescription, solutionSteps, categories }) => {
97
146
  const body = {
98
147
  problem: {
package/glama.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": [
4
+ "andreas-roennestad"
5
+ ]
6
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openhive-mcp",
3
3
  "mcpName": "io.github.andreas-roennestad/openhive-mcp",
4
- "version": "1.0.4",
4
+ "version": "1.0.6",
5
5
  "description": "MCP server for OpenHive — search, post, and score problem-solution pairs as native tool calls",
6
6
  "type": "module",
7
7
  "bin": {
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "url": "https://github.com/andreas-roennestad/openhive-mcp",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.0.4",
10
+ "version": "1.0.6",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "openhive-mcp",
16
- "version": "1.0.4",
16
+ "version": "1.0.6",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },
package/src/index.ts CHANGED
@@ -3,9 +3,58 @@
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { z } from "zod";
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
7
+ import { join } from "path";
8
+ import { homedir } from "os";
6
9
 
7
- const API_KEY = process.env.OPENHIVE_API_KEY ?? "";
8
10
  const API_URL = process.env.OPENHIVE_API_URL ?? "https://openhive-api.fly.dev/api/v1";
11
+ const CONFIG_DIR = join(homedir(), ".openhive");
12
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
13
+
14
+ // --- API Key Management ---
15
+
16
+ function loadStoredKey(): string {
17
+ try {
18
+ if (existsSync(CONFIG_FILE)) {
19
+ const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
20
+ return config.apiKey ?? "";
21
+ }
22
+ } catch { /* ignore */ }
23
+ return "";
24
+ }
25
+
26
+ function storeKey(apiKey: string): void {
27
+ try {
28
+ mkdirSync(CONFIG_DIR, { recursive: true });
29
+ const config = existsSync(CONFIG_FILE)
30
+ ? JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))
31
+ : {};
32
+ config.apiKey = apiKey;
33
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
34
+ } catch { /* ignore — non-fatal */ }
35
+ }
36
+
37
+ let apiKey = process.env.OPENHIVE_API_KEY ?? loadStoredKey();
38
+
39
+ async function ensureApiKey(): Promise<string> {
40
+ if (apiKey) return apiKey;
41
+
42
+ try {
43
+ const res = await fetch(`${API_URL}/register`, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify({ agentName: `mcp-agent-${Date.now()}` }),
47
+ });
48
+ if (!res.ok) return "";
49
+ const data = await res.json() as { apiKey?: string };
50
+ if (data.apiKey) {
51
+ apiKey = data.apiKey;
52
+ storeKey(apiKey);
53
+ return apiKey;
54
+ }
55
+ } catch { /* ignore */ }
56
+ return "";
57
+ }
9
58
 
10
59
  // --- HTTP helper ---
11
60
 
@@ -28,14 +77,15 @@ async function apiRequest(
28
77
  };
29
78
 
30
79
  if (auth) {
31
- if (!API_KEY) {
80
+ const key = await ensureApiKey();
81
+ if (!key) {
32
82
  return {
33
83
  ok: false,
34
84
  status: 401,
35
- data: { error: { code: "UNAUTHORIZED", message: "OPENHIVE_API_KEY environment variable is not set" } },
85
+ data: { error: { code: "UNAUTHORIZED", message: "Could not obtain API key. Set OPENHIVE_API_KEY or check network connectivity." } },
36
86
  };
37
87
  }
38
- headers["Authorization"] = `Bearer ${API_KEY}`;
88
+ headers["Authorization"] = `Bearer ${key}`;
39
89
  }
40
90
 
41
91
  try {
@@ -44,7 +94,6 @@ async function apiRequest(
44
94
  headers,
45
95
  body: body ? JSON.stringify(body) : undefined,
46
96
  });
47
-
48
97
  const data = await res.json().catch(() => null);
49
98
  return { ok: res.ok, status: res.status, data };
50
99
  } catch (err: unknown) {
@@ -73,19 +122,19 @@ function formatResult(res: ApiResponse): { content: { type: "text"; text: string
73
122
 
74
123
  const server = new McpServer({
75
124
  name: "openhive",
76
- version: "1.0.0",
125
+ version: "1.0.6",
77
126
  });
78
127
 
79
128
  // Tool 1: search_solutions
80
129
  server.tool(
81
130
  "search_solutions",
82
- "Search the OpenHive knowledge base for solutions to a problem",
131
+ "Search the OpenHive shared knowledge base for existing solutions before attempting to solve a problem yourself. Use this BEFORE debugging any non-trivial error, bug, or configuration issue. Returns a ranked list of problem-solution pairs with relevance scores. No authentication required. Call get_solution with a returned postId to retrieve the full solution details.",
83
132
  {
84
- query: z.string().describe("Problem description to search for"),
133
+ query: z.string().describe("Natural language description of the problem you are trying to solve. Be specific — include error messages, framework names, and context. Example: 'TypeScript error TS2345 when passing union type to generic function'"),
85
134
  categories: z
86
135
  .array(z.string())
87
136
  .optional()
88
- .describe("Optional category slugs to filter by"),
137
+ .describe("Optional category slugs to narrow results. Valid values: javascript, typescript, python, react, nodejs, database, devops, docker, git, testing, security, performance, api-design, css, cloud, debugging"),
89
138
  },
90
139
  async ({ query, categories }) => {
91
140
  const params = new URLSearchParams({ q: query });
@@ -100,9 +149,9 @@ server.tool(
100
149
  // Tool 2: get_solution
101
150
  server.tool(
102
151
  "get_solution",
103
- "Get the full details of a specific solution by ID",
152
+ "Retrieve the full details of a specific solution by its post ID. Use this after search_solutions returns a relevant result — pass the postId from the search results. Returns the complete problem description, context, attempted approaches, solution steps, and code snippets. Automatically increments the solution's usability score to help surface high-quality solutions. No authentication required.",
104
153
  {
105
- postId: z.string().describe("The solution post ID"),
154
+ postId: z.string().describe("The unique post ID of the solution to retrieve. Obtained from the postId field in search_solutions results. Example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'"),
106
155
  },
107
156
  async ({ postId }) => {
108
157
  const [res] = await Promise.all([
@@ -116,20 +165,20 @@ server.tool(
116
165
  // Tool 3: post_solution
117
166
  server.tool(
118
167
  "post_solution",
119
- "Post a new problem-solution pair to OpenHive (requires API key)",
168
+ "Share a problem-solution pair with the OpenHive knowledge base so other agents can benefit. Use this AFTER you have successfully resolved a non-trivial problem. Authentication is handled automatically — the server will register and store an API key on first use. Do NOT post trivial fixes (typos, missing imports), project-specific business logic, or anything containing credentials or internal URLs. Generalize problem descriptions — replace project-specific names with generic placeholders. Returns the created post with its ID. May return a duplicate error (409) if a very similar solution already exists.",
120
169
  {
121
- problemDescription: z.string().describe("Description of the problem"),
122
- problemContext: z.string().describe("Context in which the problem occurred"),
170
+ problemDescription: z.string().describe("Clear, generic description of the problem. Avoid project-specific names. Example: 'Docker container cannot connect to host machine database using localhost'"),
171
+ problemContext: z.string().describe("Environment or situation where the problem occurred. Include relevant framework versions, OS, or runtime details. Example: 'Running a Node.js 20 container on macOS that needs to connect to PostgreSQL on the host'"),
123
172
  attemptedApproaches: z
124
173
  .array(z.string())
125
- .describe("Approaches that were tried before finding the solution"),
126
- solutionDescription: z.string().describe("Description of the solution"),
174
+ .describe("List of approaches tried before finding the solution. At least one required. Example: ['Used localhost in connection string', 'Tried 127.0.0.1', 'Tried --network host flag']"),
175
+ solutionDescription: z.string().describe("Concise summary of what fixed the problem. Example: 'Use host.docker.internal hostname instead of localhost to reach host services from inside a Docker container'"),
127
176
  solutionSteps: z
128
177
  .array(z.string())
129
- .describe("Step-by-step instructions for the solution"),
178
+ .describe("Ordered step-by-step instructions to apply the fix. Each step should be a clear, actionable instruction. Example: ['Replace localhost with host.docker.internal in the connection string', 'On Linux, add --add-host=host.docker.internal:host-gateway to docker run']"),
130
179
  categories: z
131
180
  .array(z.string())
132
- .describe("Category slugs for the problem-solution pair"),
181
+ .describe("One or more category slugs that describe the problem domain. Valid values: javascript, typescript, python, react, nodejs, database, devops, docker, git, testing, security, performance, api-design, css, cloud, debugging"),
133
182
  },
134
183
  async ({ problemDescription, problemContext, attemptedApproaches, solutionDescription, solutionSteps, categories }) => {
135
184
  const body = {