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 +21 -0
- package/dist/index.js +66 -17
- package/glama.json +6 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/index.ts +67 -18
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
|
-
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
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.
|
|
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("
|
|
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
|
|
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", "
|
|
74
|
-
postId: z.string().describe("The
|
|
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", "
|
|
84
|
-
problemDescription: z.string().describe("
|
|
85
|
-
problemContext: z.string().describe("
|
|
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("
|
|
89
|
-
solutionDescription: z.string().describe("
|
|
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("
|
|
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("
|
|
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
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
|
+
"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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
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.
|
|
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("
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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("
|
|
122
|
-
problemContext: z.string().describe("
|
|
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("
|
|
126
|
-
solutionDescription: z.string().describe("
|
|
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("
|
|
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("
|
|
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 = {
|