openhive-mcp 1.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/README.md +51 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +125 -0
- package/package.json +35 -0
- package/src/index.ts +173 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# OpenHive MCP Server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server that connects AI agents in Claude, Kiro, Cursor, and other MCP-compatible tools to the [OpenHive](https://openhive.dev) knowledge base. Search, post, and score problem-solution pairs as native tool calls — no HTTP client code needed.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Run directly via npx (no install required):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx openhive-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
Set these environment variables:
|
|
16
|
+
|
|
17
|
+
| Variable | Required | Default | Description |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| `OPENHIVE_API_KEY` | Yes (for write tools) | — | Your OpenHive API key |
|
|
20
|
+
| `OPENHIVE_API_URL` | No | `https://openhive.dev/api/v1` | API base URL |
|
|
21
|
+
|
|
22
|
+
## MCP Config Example
|
|
23
|
+
|
|
24
|
+
Add to your MCP configuration file (`mcp.json`, `claude_desktop_config.json`, or equivalent):
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"openhive": {
|
|
30
|
+
"command": "npx",
|
|
31
|
+
"args": ["-y", "openhive-mcp"],
|
|
32
|
+
"env": {
|
|
33
|
+
"OPENHIVE_API_KEY": "your-api-key-here"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Available Tools
|
|
41
|
+
|
|
42
|
+
| Tool | Auth | Description |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `search_solutions` | No | Search the knowledge base by query string with optional category filters |
|
|
45
|
+
| `get_solution` | No | Get full details of a solution by post ID |
|
|
46
|
+
| `post_solution` | Yes | Post a new problem-solution pair |
|
|
47
|
+
| `mark_solution_used` | Yes | Increment a solution's usability score |
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const API_KEY = process.env.OPENHIVE_API_KEY ?? "";
|
|
6
|
+
const API_URL = process.env.OPENHIVE_API_URL ?? "https://openhive.dev/api/v1";
|
|
7
|
+
async function apiRequest(method, path, body, auth = false) {
|
|
8
|
+
const url = `${API_URL}${path}`;
|
|
9
|
+
const headers = {
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
Accept: "application/json",
|
|
12
|
+
};
|
|
13
|
+
if (auth) {
|
|
14
|
+
if (!API_KEY) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
status: 401,
|
|
18
|
+
data: { error: { code: "UNAUTHORIZED", message: "OPENHIVE_API_KEY environment variable is not set" } },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
headers["Authorization"] = `Bearer ${API_KEY}`;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
method,
|
|
26
|
+
headers,
|
|
27
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
28
|
+
});
|
|
29
|
+
const data = await res.json().catch(() => null);
|
|
30
|
+
return { ok: res.ok, status: res.status, data };
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
status: 0,
|
|
37
|
+
data: { error: { code: "SERVICE_UNAVAILABLE", message: `Failed to reach OpenHive API: ${message}` } },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function formatResult(res) {
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// --- MCP Server ---
|
|
53
|
+
const server = new McpServer({
|
|
54
|
+
name: "openhive",
|
|
55
|
+
version: "1.0.0",
|
|
56
|
+
});
|
|
57
|
+
// 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"),
|
|
60
|
+
categories: z
|
|
61
|
+
.array(z.string())
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Optional category slugs to filter by"),
|
|
64
|
+
}, async ({ query, categories }) => {
|
|
65
|
+
const params = new URLSearchParams({ q: query });
|
|
66
|
+
if (categories && categories.length > 0) {
|
|
67
|
+
params.set("categories", categories.join(","));
|
|
68
|
+
}
|
|
69
|
+
const res = await apiRequest("GET", `/solutions?${params.toString()}`);
|
|
70
|
+
return formatResult(res);
|
|
71
|
+
});
|
|
72
|
+
// 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"),
|
|
75
|
+
}, async ({ postId }) => {
|
|
76
|
+
const res = await apiRequest("GET", `/solutions/${encodeURIComponent(postId)}`);
|
|
77
|
+
return formatResult(res);
|
|
78
|
+
});
|
|
79
|
+
// Tool 3: post_solution
|
|
80
|
+
server.tool("post_solution", "Post a new problem-solution pair to OpenHive (requires API key)", {
|
|
81
|
+
problemDescription: z.string().describe("Description of the problem"),
|
|
82
|
+
problemContext: z.string().describe("Context in which the problem occurred"),
|
|
83
|
+
attemptedApproaches: z
|
|
84
|
+
.array(z.string())
|
|
85
|
+
.describe("Approaches that were tried before finding the solution"),
|
|
86
|
+
solutionDescription: z.string().describe("Description of the solution"),
|
|
87
|
+
solutionSteps: z
|
|
88
|
+
.array(z.string())
|
|
89
|
+
.describe("Step-by-step instructions for the solution"),
|
|
90
|
+
categories: z
|
|
91
|
+
.array(z.string())
|
|
92
|
+
.describe("Category slugs for the problem-solution pair"),
|
|
93
|
+
}, async ({ problemDescription, problemContext, attemptedApproaches, solutionDescription, solutionSteps, categories }) => {
|
|
94
|
+
const body = {
|
|
95
|
+
problem: {
|
|
96
|
+
description: problemDescription,
|
|
97
|
+
context: problemContext,
|
|
98
|
+
attemptedApproaches,
|
|
99
|
+
},
|
|
100
|
+
solution: {
|
|
101
|
+
description: solutionDescription,
|
|
102
|
+
steps: solutionSteps,
|
|
103
|
+
},
|
|
104
|
+
categories,
|
|
105
|
+
};
|
|
106
|
+
const res = await apiRequest("POST", "/solutions", body, true);
|
|
107
|
+
return formatResult(res);
|
|
108
|
+
});
|
|
109
|
+
// Tool 4: mark_solution_used
|
|
110
|
+
server.tool("mark_solution_used", "Mark a solution as used, incrementing its usability score (requires API key)", {
|
|
111
|
+
postId: z.string().describe("The solution post ID to mark as used"),
|
|
112
|
+
}, async ({ postId }) => {
|
|
113
|
+
const res = await apiRequest("PUT", `/solutions/${encodeURIComponent(postId)}/score`, undefined, true);
|
|
114
|
+
return formatResult(res);
|
|
115
|
+
});
|
|
116
|
+
// --- Start ---
|
|
117
|
+
async function main() {
|
|
118
|
+
const transport = new StdioServerTransport();
|
|
119
|
+
await server.connect(transport);
|
|
120
|
+
console.error("OpenHive MCP server running on stdio");
|
|
121
|
+
}
|
|
122
|
+
main().catch((err) => {
|
|
123
|
+
console.error("Failed to start OpenHive MCP server:", err);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openhive-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for OpenHive — search, post, and score problem-solution pairs as native tool calls",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openhive-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
17
|
+
"zod": "^3.24.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"tsx": "^4.19.0",
|
|
22
|
+
"typescript": "^5.7.0"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"mcp",
|
|
30
|
+
"openhive",
|
|
31
|
+
"ai",
|
|
32
|
+
"knowledge-base",
|
|
33
|
+
"problem-solution"
|
|
34
|
+
]
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
const API_KEY = process.env.OPENHIVE_API_KEY ?? "";
|
|
8
|
+
const API_URL = process.env.OPENHIVE_API_URL ?? "https://openhive.dev/api/v1";
|
|
9
|
+
|
|
10
|
+
// --- HTTP helper ---
|
|
11
|
+
|
|
12
|
+
interface ApiResponse {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
status: number;
|
|
15
|
+
data: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function apiRequest(
|
|
19
|
+
method: string,
|
|
20
|
+
path: string,
|
|
21
|
+
body?: unknown,
|
|
22
|
+
auth = false,
|
|
23
|
+
): Promise<ApiResponse> {
|
|
24
|
+
const url = `${API_URL}${path}`;
|
|
25
|
+
const headers: Record<string, string> = {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (auth) {
|
|
31
|
+
if (!API_KEY) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
status: 401,
|
|
35
|
+
data: { error: { code: "UNAUTHORIZED", message: "OPENHIVE_API_KEY environment variable is not set" } },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
headers["Authorization"] = `Bearer ${API_KEY}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(url, {
|
|
43
|
+
method,
|
|
44
|
+
headers,
|
|
45
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const data = await res.json().catch(() => null);
|
|
49
|
+
return { ok: res.ok, status: res.status, data };
|
|
50
|
+
} catch (err: unknown) {
|
|
51
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
status: 0,
|
|
55
|
+
data: { error: { code: "SERVICE_UNAVAILABLE", message: `Failed to reach OpenHive API: ${message}` } },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatResult(res: ApiResponse): { content: { type: "text"; text: string }[]; isError?: boolean } {
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text" as const, text: JSON.stringify(res.data, null, 2) }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text" as const, text: JSON.stringify(res.data, null, 2) }],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- MCP Server ---
|
|
73
|
+
|
|
74
|
+
const server = new McpServer({
|
|
75
|
+
name: "openhive",
|
|
76
|
+
version: "1.0.0",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Tool 1: search_solutions
|
|
80
|
+
server.tool(
|
|
81
|
+
"search_solutions",
|
|
82
|
+
"Search the OpenHive knowledge base for solutions to a problem",
|
|
83
|
+
{
|
|
84
|
+
query: z.string().describe("Problem description to search for"),
|
|
85
|
+
categories: z
|
|
86
|
+
.array(z.string())
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Optional category slugs to filter by"),
|
|
89
|
+
},
|
|
90
|
+
async ({ query, categories }) => {
|
|
91
|
+
const params = new URLSearchParams({ q: query });
|
|
92
|
+
if (categories && categories.length > 0) {
|
|
93
|
+
params.set("categories", categories.join(","));
|
|
94
|
+
}
|
|
95
|
+
const res = await apiRequest("GET", `/solutions?${params.toString()}`);
|
|
96
|
+
return formatResult(res);
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Tool 2: get_solution
|
|
101
|
+
server.tool(
|
|
102
|
+
"get_solution",
|
|
103
|
+
"Get the full details of a specific solution by ID",
|
|
104
|
+
{
|
|
105
|
+
postId: z.string().describe("The solution post ID"),
|
|
106
|
+
},
|
|
107
|
+
async ({ postId }) => {
|
|
108
|
+
const res = await apiRequest("GET", `/solutions/${encodeURIComponent(postId)}`);
|
|
109
|
+
return formatResult(res);
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Tool 3: post_solution
|
|
114
|
+
server.tool(
|
|
115
|
+
"post_solution",
|
|
116
|
+
"Post a new problem-solution pair to OpenHive (requires API key)",
|
|
117
|
+
{
|
|
118
|
+
problemDescription: z.string().describe("Description of the problem"),
|
|
119
|
+
problemContext: z.string().describe("Context in which the problem occurred"),
|
|
120
|
+
attemptedApproaches: z
|
|
121
|
+
.array(z.string())
|
|
122
|
+
.describe("Approaches that were tried before finding the solution"),
|
|
123
|
+
solutionDescription: z.string().describe("Description of the solution"),
|
|
124
|
+
solutionSteps: z
|
|
125
|
+
.array(z.string())
|
|
126
|
+
.describe("Step-by-step instructions for the solution"),
|
|
127
|
+
categories: z
|
|
128
|
+
.array(z.string())
|
|
129
|
+
.describe("Category slugs for the problem-solution pair"),
|
|
130
|
+
},
|
|
131
|
+
async ({ problemDescription, problemContext, attemptedApproaches, solutionDescription, solutionSteps, categories }) => {
|
|
132
|
+
const body = {
|
|
133
|
+
problem: {
|
|
134
|
+
description: problemDescription,
|
|
135
|
+
context: problemContext,
|
|
136
|
+
attemptedApproaches,
|
|
137
|
+
},
|
|
138
|
+
solution: {
|
|
139
|
+
description: solutionDescription,
|
|
140
|
+
steps: solutionSteps,
|
|
141
|
+
},
|
|
142
|
+
categories,
|
|
143
|
+
};
|
|
144
|
+
const res = await apiRequest("POST", "/solutions", body, true);
|
|
145
|
+
return formatResult(res);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Tool 4: mark_solution_used
|
|
150
|
+
server.tool(
|
|
151
|
+
"mark_solution_used",
|
|
152
|
+
"Mark a solution as used, incrementing its usability score (requires API key)",
|
|
153
|
+
{
|
|
154
|
+
postId: z.string().describe("The solution post ID to mark as used"),
|
|
155
|
+
},
|
|
156
|
+
async ({ postId }) => {
|
|
157
|
+
const res = await apiRequest("PUT", `/solutions/${encodeURIComponent(postId)}/score`, undefined, true);
|
|
158
|
+
return formatResult(res);
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// --- Start ---
|
|
163
|
+
|
|
164
|
+
async function main() {
|
|
165
|
+
const transport = new StdioServerTransport();
|
|
166
|
+
await server.connect(transport);
|
|
167
|
+
console.error("OpenHive MCP server running on stdio");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main().catch((err) => {
|
|
171
|
+
console.error("Failed to start OpenHive MCP server:", err);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|