@ikhono/mcp 0.2.0 → 0.2.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/README.md +35 -7
- package/dist/client.d.ts +24 -0
- package/dist/client.js +72 -0
- package/dist/index.js +94 -86
- package/dist/server.d.ts +6 -0
- package/dist/server.js +191 -0
- package/package.json +12 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ikhono/mcp
|
|
2
2
|
|
|
3
|
-
MCP server for [iKhono](https://github.com/ikhono/ikhono) — a runtime skill router that gives AI agents access to community-built skills on the fly.
|
|
3
|
+
MCP server for [iKhono](https://github.com/ikhono-ai/ikhono) — a runtime skill router that gives AI agents access to community-built skills on the fly.
|
|
4
4
|
|
|
5
5
|
No local skill installation needed. Your AI agent searches, loads, and follows skills directly from the iKhono registry.
|
|
6
6
|
|
|
@@ -62,7 +62,7 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
62
62
|
To use authenticated features (pinning, rating, publishing your own skills), log in with the CLI first:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
npx @ikhono/cli login
|
|
65
|
+
npx @ikhono/cli login # GitHub SSO (opens browser)
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
The MCP server automatically reads your token from `~/.ikhono/config.json`.
|
|
@@ -71,7 +71,7 @@ You can also pass credentials directly:
|
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
73
|
# Via CLI args
|
|
74
|
-
npx @ikhono/mcp --token YOUR_TOKEN --api-url https://
|
|
74
|
+
npx @ikhono/mcp --token YOUR_TOKEN --api-url https://ikhono.io
|
|
75
75
|
|
|
76
76
|
# Via environment variables
|
|
77
77
|
IKHONO_API_TOKEN=YOUR_TOKEN npx @ikhono/mcp
|
|
@@ -132,6 +132,34 @@ Rate a skill after using it.
|
|
|
132
132
|
| `stars` | number | Yes | Rating from 1 to 5 |
|
|
133
133
|
| `review` | string | No | Optional text review |
|
|
134
134
|
|
|
135
|
+
## Streamable HTTP Transport
|
|
136
|
+
|
|
137
|
+
The iKhono MCP server is also available as a hosted HTTP endpoint, useful for MCP registries (Smithery, etc.) and web-based clients:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
POST https://ikhono.io/mcp
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Headers:
|
|
144
|
+
- `Content-Type: application/json`
|
|
145
|
+
- `Accept: application/json, text/event-stream`
|
|
146
|
+
- `Authorization: Bearer YOUR_TOKEN` (optional, for authenticated operations)
|
|
147
|
+
|
|
148
|
+
Sessions are stateful — the server returns an `Mcp-Session-Id` header to include in subsequent requests.
|
|
149
|
+
|
|
150
|
+
### Programmatic Usage
|
|
151
|
+
|
|
152
|
+
The server factory and client are available as package exports for embedding in your own server:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { createMcpServer } from '@ikhono/mcp/server';
|
|
156
|
+
import { IkhonoClient } from '@ikhono/mcp/client';
|
|
157
|
+
|
|
158
|
+
const client = new IkhonoClient({ apiUrl: 'https://ikhono.io', token: 'optional' });
|
|
159
|
+
const server = createMcpServer(client);
|
|
160
|
+
// Connect your own transport...
|
|
161
|
+
```
|
|
162
|
+
|
|
135
163
|
## How It Works
|
|
136
164
|
|
|
137
165
|
1. Your AI agent receives a task (e.g., "review this code")
|
|
@@ -145,10 +173,10 @@ Skills are community-created Markdown documents with structured processes, check
|
|
|
145
173
|
|
|
146
174
|
## Links
|
|
147
175
|
|
|
148
|
-
- [Getting Started](https://github.com/ikhono/ikhono/blob/main/docs/getting-started.md)
|
|
149
|
-
- [Creating Skills](https://github.com/ikhono/ikhono/blob/main/docs/creating-skills.md)
|
|
150
|
-
- [CLI Reference](https://github.com/ikhono/ikhono/blob/main/docs/cli-reference.md)
|
|
151
|
-
- [GitHub](https://github.com/ikhono/ikhono)
|
|
176
|
+
- [Getting Started](https://github.com/ikhono-ai/ikhono/blob/main/docs/getting-started.md)
|
|
177
|
+
- [Creating Skills](https://github.com/ikhono-ai/ikhono/blob/main/docs/creating-skills.md)
|
|
178
|
+
- [CLI Reference](https://github.com/ikhono-ai/ikhono/blob/main/docs/cli-reference.md)
|
|
179
|
+
- [GitHub](https://github.com/ikhono-ai/ikhono)
|
|
152
180
|
|
|
153
181
|
## License
|
|
154
182
|
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
interface IkhonoClientConfig {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
token?: string;
|
|
4
|
+
}
|
|
5
|
+
declare class IkhonoClient {
|
|
6
|
+
private apiUrl;
|
|
7
|
+
private token?;
|
|
8
|
+
constructor(config: IkhonoClientConfig);
|
|
9
|
+
private headers;
|
|
10
|
+
private handleResponse;
|
|
11
|
+
searchSkills(query: string, options?: {
|
|
12
|
+
category?: string;
|
|
13
|
+
author?: string;
|
|
14
|
+
mine?: boolean;
|
|
15
|
+
limit?: number;
|
|
16
|
+
}): Promise<unknown>;
|
|
17
|
+
getSkill(slug: string): Promise<unknown>;
|
|
18
|
+
pinSkill(slug: string): Promise<unknown>;
|
|
19
|
+
unpinSkill(slug: string): Promise<unknown>;
|
|
20
|
+
listPinned(): Promise<unknown>;
|
|
21
|
+
rateSkill(slug: string, stars: number, review?: string): Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { IkhonoClient, type IkhonoClientConfig };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var IkhonoClient = class {
|
|
3
|
+
apiUrl;
|
|
4
|
+
token;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.apiUrl = config.apiUrl.replace(/\/$/, "");
|
|
7
|
+
this.token = config.token;
|
|
8
|
+
}
|
|
9
|
+
headers() {
|
|
10
|
+
const h = { "Content-Type": "application/json" };
|
|
11
|
+
if (this.token) {
|
|
12
|
+
h["Authorization"] = `Bearer ${this.token}`;
|
|
13
|
+
}
|
|
14
|
+
return h;
|
|
15
|
+
}
|
|
16
|
+
async handleResponse(res, action) {
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
let message = res.statusText;
|
|
19
|
+
try {
|
|
20
|
+
const body = await res.json();
|
|
21
|
+
if (body.error) message = typeof body.error === "string" ? body.error : JSON.stringify(body.error);
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`${action}: ${message}`);
|
|
25
|
+
}
|
|
26
|
+
const json = await res.json();
|
|
27
|
+
return json.data;
|
|
28
|
+
}
|
|
29
|
+
async searchSkills(query, options) {
|
|
30
|
+
const params = new URLSearchParams();
|
|
31
|
+
if (query) params.set("q", query);
|
|
32
|
+
if (options?.category) params.set("category", options.category);
|
|
33
|
+
if (options?.author) params.set("author", options.author);
|
|
34
|
+
if (options?.mine) params.set("mine", "true");
|
|
35
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
36
|
+
const res = await fetch(`${this.apiUrl}/api/skills?${params}`, { headers: this.headers() });
|
|
37
|
+
return this.handleResponse(res, "Search failed");
|
|
38
|
+
}
|
|
39
|
+
async getSkill(slug) {
|
|
40
|
+
const res = await fetch(`${this.apiUrl}/api/skills/${slug}?use=true`, { headers: this.headers() });
|
|
41
|
+
return this.handleResponse(res, "Get skill failed");
|
|
42
|
+
}
|
|
43
|
+
async pinSkill(slug) {
|
|
44
|
+
const res = await fetch(`${this.apiUrl}/api/skills/${slug}/pin`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: this.headers()
|
|
47
|
+
});
|
|
48
|
+
return this.handleResponse(res, "Pin failed");
|
|
49
|
+
}
|
|
50
|
+
async unpinSkill(slug) {
|
|
51
|
+
const res = await fetch(`${this.apiUrl}/api/skills/${slug}/pin`, {
|
|
52
|
+
method: "DELETE",
|
|
53
|
+
headers: this.headers()
|
|
54
|
+
});
|
|
55
|
+
return this.handleResponse(res, "Unpin failed");
|
|
56
|
+
}
|
|
57
|
+
async listPinned() {
|
|
58
|
+
const res = await fetch(`${this.apiUrl}/api/skills/pinned`, { headers: this.headers() });
|
|
59
|
+
return this.handleResponse(res, "List pins failed");
|
|
60
|
+
}
|
|
61
|
+
async rateSkill(slug, stars, review) {
|
|
62
|
+
const res = await fetch(`${this.apiUrl}/api/skills/${slug}/rate`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: this.headers(),
|
|
65
|
+
body: JSON.stringify({ stars, review })
|
|
66
|
+
});
|
|
67
|
+
return this.handleResponse(res, "Rate failed");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
export {
|
|
71
|
+
IkhonoClient
|
|
72
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
8
|
|
|
10
9
|
// src/client.ts
|
|
@@ -77,6 +76,9 @@ var IkhonoClient = class {
|
|
|
77
76
|
}
|
|
78
77
|
};
|
|
79
78
|
|
|
79
|
+
// src/server.ts
|
|
80
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
81
|
+
|
|
80
82
|
// src/tools/search.ts
|
|
81
83
|
import { z } from "zod";
|
|
82
84
|
var searchToolName = "ikhono_skill_search";
|
|
@@ -168,12 +170,101 @@ async function handleRate(client2, args2) {
|
|
|
168
170
|
// src/version.ts
|
|
169
171
|
import { createRequire } from "module";
|
|
170
172
|
function getVersion() {
|
|
171
|
-
if (true) return "0.2.
|
|
173
|
+
if (true) return "0.2.6";
|
|
172
174
|
const require2 = createRequire(import.meta.url);
|
|
173
175
|
return require2("../package.json").version;
|
|
174
176
|
}
|
|
175
177
|
var VERSION = getVersion();
|
|
176
178
|
|
|
179
|
+
// src/server.ts
|
|
180
|
+
function createMcpServer(client2) {
|
|
181
|
+
const server2 = new McpServer({
|
|
182
|
+
name: "iKhono",
|
|
183
|
+
version: VERSION
|
|
184
|
+
}, {
|
|
185
|
+
instructions: `You have access to iKhono, a registry of AI skills that enhance your capabilities. When the user asks you to do something that could benefit from specialized expertise (code review, test writing, documentation, refactoring, etc.), use ikhono_skill_search to find relevant skills, then use ikhono_skill_get to load the skill's full instructions. Follow those instructions to give a better, more structured response. Always tell the user which skill you're using and its rating.`
|
|
186
|
+
});
|
|
187
|
+
server2.registerTool(searchToolName, {
|
|
188
|
+
description: searchToolDescription,
|
|
189
|
+
inputSchema: searchToolSchema.shape
|
|
190
|
+
}, async (args2) => {
|
|
191
|
+
try {
|
|
192
|
+
const results = await handleSearch(client2, searchToolSchema.parse(args2));
|
|
193
|
+
return { content: [{ type: "text", text: formatSearchResults(results) }] };
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
server2.registerTool(getSkillToolName, {
|
|
199
|
+
description: getSkillToolDescription,
|
|
200
|
+
inputSchema: getSkillToolSchema.shape
|
|
201
|
+
}, async (args2) => {
|
|
202
|
+
try {
|
|
203
|
+
const parsed = getSkillToolSchema.parse(args2);
|
|
204
|
+
const skill = await handleGetSkill(client2, parsed);
|
|
205
|
+
const slug = parsed.slug;
|
|
206
|
+
const prompt = `IMPORTANT: After you finish the task using this skill, you MUST ask the user:
|
|
207
|
+
"Would you like to pin or rate the skill ${slug}? Rate it 1-5 stars."
|
|
208
|
+
If they respond with a rating, call ikhono_skill_rate with slug "${slug}" and their star count.
|
|
209
|
+
If they want to pin it, call ikhono_skill_pin with slug "${slug}".
|
|
210
|
+
This helps skill creators improve their work.`;
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{ type: "text", text: JSON.stringify(skill, null, 2) },
|
|
214
|
+
{ type: "text", text: prompt }
|
|
215
|
+
]
|
|
216
|
+
};
|
|
217
|
+
} catch (err) {
|
|
218
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
server2.registerTool(pinToolName, {
|
|
222
|
+
description: pinToolDescription,
|
|
223
|
+
inputSchema: pinToolSchema.shape
|
|
224
|
+
}, async (args2) => {
|
|
225
|
+
try {
|
|
226
|
+
const result = await handlePin(client2, pinToolSchema.parse(args2));
|
|
227
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
228
|
+
} catch (err) {
|
|
229
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
server2.registerTool(unpinToolName, {
|
|
233
|
+
description: unpinToolDescription,
|
|
234
|
+
inputSchema: unpinToolSchema.shape
|
|
235
|
+
}, async (args2) => {
|
|
236
|
+
try {
|
|
237
|
+
const result = await handleUnpin(client2, unpinToolSchema.parse(args2));
|
|
238
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
239
|
+
} catch (err) {
|
|
240
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
server2.registerTool(listPinnedToolName, {
|
|
244
|
+
description: listPinnedToolDescription,
|
|
245
|
+
inputSchema: listPinnedToolSchema.shape
|
|
246
|
+
}, async () => {
|
|
247
|
+
try {
|
|
248
|
+
const result = await handleListPinned(client2);
|
|
249
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
250
|
+
} catch (err) {
|
|
251
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
server2.registerTool(rateToolName, {
|
|
255
|
+
description: rateToolDescription,
|
|
256
|
+
inputSchema: rateToolSchema.shape
|
|
257
|
+
}, async (args2) => {
|
|
258
|
+
try {
|
|
259
|
+
const result = await handleRate(client2, rateToolSchema.parse(args2));
|
|
260
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
261
|
+
} catch (err) {
|
|
262
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return server2;
|
|
266
|
+
}
|
|
267
|
+
|
|
177
268
|
// src/index.ts
|
|
178
269
|
function readCliConfig() {
|
|
179
270
|
try {
|
|
@@ -199,89 +290,6 @@ for (let i = 0; i < args.length; i++) {
|
|
|
199
290
|
apiUrl = process.env.IKHONO_API_URL || apiUrl || cliConfig.apiUrl || "https://ikhono.io";
|
|
200
291
|
token = process.env.IKHONO_API_TOKEN || token || cliConfig.token;
|
|
201
292
|
var client = new IkhonoClient({ apiUrl, token });
|
|
202
|
-
var server =
|
|
203
|
-
name: "iKhono",
|
|
204
|
-
version: VERSION
|
|
205
|
-
}, {
|
|
206
|
-
instructions: `You have access to iKhono, a registry of AI skills that enhance your capabilities. When the user asks you to do something that could benefit from specialized expertise (code review, test writing, documentation, refactoring, etc.), use ikhono_skill_search to find relevant skills, then use ikhono_skill_get to load the skill's full instructions. Follow those instructions to give a better, more structured response. Always tell the user which skill you're using and its rating.`
|
|
207
|
-
});
|
|
208
|
-
server.registerTool(searchToolName, {
|
|
209
|
-
description: searchToolDescription,
|
|
210
|
-
inputSchema: searchToolSchema.shape
|
|
211
|
-
}, async (args2) => {
|
|
212
|
-
try {
|
|
213
|
-
const results = await handleSearch(client, searchToolSchema.parse(args2));
|
|
214
|
-
return { content: [{ type: "text", text: formatSearchResults(results) }] };
|
|
215
|
-
} catch (err) {
|
|
216
|
-
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
server.registerTool(getSkillToolName, {
|
|
220
|
-
description: getSkillToolDescription,
|
|
221
|
-
inputSchema: getSkillToolSchema.shape
|
|
222
|
-
}, async (args2) => {
|
|
223
|
-
try {
|
|
224
|
-
const parsed = getSkillToolSchema.parse(args2);
|
|
225
|
-
const skill = await handleGetSkill(client, parsed);
|
|
226
|
-
const slug = parsed.slug;
|
|
227
|
-
const prompt = `IMPORTANT: After you finish the task using this skill, you MUST ask the user:
|
|
228
|
-
"Would you like to pin or rate the skill ${slug}? Rate it 1-5 stars."
|
|
229
|
-
If they respond with a rating, call ikhono_skill_rate with slug "${slug}" and their star count.
|
|
230
|
-
If they want to pin it, call ikhono_skill_pin with slug "${slug}".
|
|
231
|
-
This helps skill creators improve their work.`;
|
|
232
|
-
return {
|
|
233
|
-
content: [
|
|
234
|
-
{ type: "text", text: JSON.stringify(skill, null, 2) },
|
|
235
|
-
{ type: "text", text: prompt }
|
|
236
|
-
]
|
|
237
|
-
};
|
|
238
|
-
} catch (err) {
|
|
239
|
-
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
server.registerTool(pinToolName, {
|
|
243
|
-
description: pinToolDescription,
|
|
244
|
-
inputSchema: pinToolSchema.shape
|
|
245
|
-
}, async (args2) => {
|
|
246
|
-
try {
|
|
247
|
-
const result = await handlePin(client, pinToolSchema.parse(args2));
|
|
248
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
249
|
-
} catch (err) {
|
|
250
|
-
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
server.registerTool(unpinToolName, {
|
|
254
|
-
description: unpinToolDescription,
|
|
255
|
-
inputSchema: unpinToolSchema.shape
|
|
256
|
-
}, async (args2) => {
|
|
257
|
-
try {
|
|
258
|
-
const result = await handleUnpin(client, unpinToolSchema.parse(args2));
|
|
259
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
260
|
-
} catch (err) {
|
|
261
|
-
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
server.registerTool(listPinnedToolName, {
|
|
265
|
-
description: listPinnedToolDescription,
|
|
266
|
-
inputSchema: listPinnedToolSchema.shape
|
|
267
|
-
}, async () => {
|
|
268
|
-
try {
|
|
269
|
-
const result = await handleListPinned(client);
|
|
270
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
271
|
-
} catch (err) {
|
|
272
|
-
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
server.registerTool(rateToolName, {
|
|
276
|
-
description: rateToolDescription,
|
|
277
|
-
inputSchema: rateToolSchema.shape
|
|
278
|
-
}, async (args2) => {
|
|
279
|
-
try {
|
|
280
|
-
const result = await handleRate(client, rateToolSchema.parse(args2));
|
|
281
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
282
|
-
} catch (err) {
|
|
283
|
-
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
284
|
-
}
|
|
285
|
-
});
|
|
293
|
+
var server = createMcpServer(client);
|
|
286
294
|
var transport = new StdioServerTransport();
|
|
287
295
|
await server.connect(transport);
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
|
|
4
|
+
// src/tools/search.ts
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
var searchToolName = "ikhono_skill_search";
|
|
7
|
+
var searchToolSchema = z.object({
|
|
8
|
+
query: z.string().optional().default("").describe('Search query to find relevant skills (e.g., "security review", "write tests", "api docs"). Leave empty when using mine or author filters to list all matching skills.'),
|
|
9
|
+
category: z.string().optional().describe('Filter by category (e.g., "security", "testing", "documentation")'),
|
|
10
|
+
author: z.string().optional().describe('Filter by author username (e.g., "@alice" or "alice")'),
|
|
11
|
+
mine: z.boolean().optional().describe("Set to true to show only your own skills (requires authentication)"),
|
|
12
|
+
limit: z.number().optional().default(3).describe("Maximum number of results to return")
|
|
13
|
+
});
|
|
14
|
+
var searchToolDescription = `Search iKhono for AI skills that match a query. Use this when the user asks you to do something that could benefit from specialized expertise. Returns a list of matching skills with their names, descriptions, ratings, usage counts, and pin counts.`;
|
|
15
|
+
function formatSearchResults(results) {
|
|
16
|
+
if (results.length === 0) {
|
|
17
|
+
return "No skills found matching your query.";
|
|
18
|
+
}
|
|
19
|
+
const lines = [`Found ${results.length} skill${results.length === 1 ? "" : "s"}:
|
|
20
|
+
`];
|
|
21
|
+
for (let i = 0; i < results.length; i++) {
|
|
22
|
+
const r = results[i];
|
|
23
|
+
const rating = r.avgRating > 0 ? `${r.avgRating.toFixed(1)}/5 (${r.ratingCount} rating${r.ratingCount === 1 ? "" : "s"})` : "no ratings yet";
|
|
24
|
+
lines.push(`${i + 1}. ${r.slug} (v${r.latestVersion})`);
|
|
25
|
+
lines.push(` ${r.description}`);
|
|
26
|
+
lines.push(` ${rating} \xB7 ${r.totalUses} uses \xB7 ${r.pinCount} pins`);
|
|
27
|
+
lines.push("");
|
|
28
|
+
}
|
|
29
|
+
lines.push("Load a skill with ikhono_skill_get using the slug.");
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
async function handleSearch(client, args) {
|
|
33
|
+
const results = await client.searchSkills(args.query, {
|
|
34
|
+
category: args.category,
|
|
35
|
+
author: args.author,
|
|
36
|
+
mine: args.mine,
|
|
37
|
+
limit: args.limit
|
|
38
|
+
});
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/tools/get-skill.ts
|
|
43
|
+
import { z as z2 } from "zod";
|
|
44
|
+
var getSkillToolName = "ikhono_skill_get";
|
|
45
|
+
var getSkillToolSchema = z2.object({
|
|
46
|
+
slug: z2.string().describe('The skill slug (e.g., "@alice/security-reviewer"). Get this from ikhono_skill_search results.')
|
|
47
|
+
});
|
|
48
|
+
var getSkillToolDescription = `Load a skill from iKhono by its slug. Returns the full skill content (instructions, process, templates) that you should follow to complete the user's task. After searching with ikhono_skill_search, use this tool to load the best matching skill.`;
|
|
49
|
+
async function handleGetSkill(client, args) {
|
|
50
|
+
const skill = await client.getSkill(args.slug);
|
|
51
|
+
return skill;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/tools/pin.ts
|
|
55
|
+
import { z as z3 } from "zod";
|
|
56
|
+
var pinToolName = "ikhono_skill_pin";
|
|
57
|
+
var pinToolSchema = z3.object({
|
|
58
|
+
slug: z3.string().describe('The skill slug to pin (e.g., "@alice/security-reviewer")')
|
|
59
|
+
});
|
|
60
|
+
var pinToolDescription = `Pin a skill to the user's favorites so it's always available. Pinned skills are shown in the user's profile.`;
|
|
61
|
+
async function handlePin(client, args) {
|
|
62
|
+
return await client.pinSkill(args.slug);
|
|
63
|
+
}
|
|
64
|
+
var unpinToolName = "ikhono_skill_unpin";
|
|
65
|
+
var unpinToolSchema = z3.object({
|
|
66
|
+
slug: z3.string().describe("The skill slug to unpin")
|
|
67
|
+
});
|
|
68
|
+
var unpinToolDescription = `Remove a skill from the user's pinned favorites.`;
|
|
69
|
+
async function handleUnpin(client, args) {
|
|
70
|
+
return await client.unpinSkill(args.slug);
|
|
71
|
+
}
|
|
72
|
+
var listPinnedToolName = "ikhono_skill_list_pinned";
|
|
73
|
+
var listPinnedToolSchema = z3.object({});
|
|
74
|
+
var listPinnedToolDescription = `List all skills the user has pinned. Returns pinned skills with their names, descriptions, and ratings.`;
|
|
75
|
+
async function handleListPinned(client) {
|
|
76
|
+
return await client.listPinned();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/tools/rate.ts
|
|
80
|
+
import { z as z4 } from "zod";
|
|
81
|
+
var rateToolName = "ikhono_skill_rate";
|
|
82
|
+
var rateToolSchema = z4.object({
|
|
83
|
+
slug: z4.string().describe('The skill slug to rate (e.g., "@alice/security-reviewer")'),
|
|
84
|
+
stars: z4.number().min(1).max(5).describe("Rating from 1 to 5 stars"),
|
|
85
|
+
review: z4.string().optional().describe("Optional text review")
|
|
86
|
+
});
|
|
87
|
+
var rateToolDescription = `Rate a skill after using it. Helps the community discover the best skills.`;
|
|
88
|
+
async function handleRate(client, args) {
|
|
89
|
+
return await client.rateSkill(args.slug, args.stars, args.review);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/version.ts
|
|
93
|
+
import { createRequire } from "module";
|
|
94
|
+
function getVersion() {
|
|
95
|
+
if (true) return "0.2.6";
|
|
96
|
+
const require2 = createRequire(import.meta.url);
|
|
97
|
+
return require2("../package.json").version;
|
|
98
|
+
}
|
|
99
|
+
var VERSION = getVersion();
|
|
100
|
+
|
|
101
|
+
// src/server.ts
|
|
102
|
+
function createMcpServer(client) {
|
|
103
|
+
const server = new McpServer({
|
|
104
|
+
name: "iKhono",
|
|
105
|
+
version: VERSION
|
|
106
|
+
}, {
|
|
107
|
+
instructions: `You have access to iKhono, a registry of AI skills that enhance your capabilities. When the user asks you to do something that could benefit from specialized expertise (code review, test writing, documentation, refactoring, etc.), use ikhono_skill_search to find relevant skills, then use ikhono_skill_get to load the skill's full instructions. Follow those instructions to give a better, more structured response. Always tell the user which skill you're using and its rating.`
|
|
108
|
+
});
|
|
109
|
+
server.registerTool(searchToolName, {
|
|
110
|
+
description: searchToolDescription,
|
|
111
|
+
inputSchema: searchToolSchema.shape
|
|
112
|
+
}, async (args) => {
|
|
113
|
+
try {
|
|
114
|
+
const results = await handleSearch(client, searchToolSchema.parse(args));
|
|
115
|
+
return { content: [{ type: "text", text: formatSearchResults(results) }] };
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
server.registerTool(getSkillToolName, {
|
|
121
|
+
description: getSkillToolDescription,
|
|
122
|
+
inputSchema: getSkillToolSchema.shape
|
|
123
|
+
}, async (args) => {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = getSkillToolSchema.parse(args);
|
|
126
|
+
const skill = await handleGetSkill(client, parsed);
|
|
127
|
+
const slug = parsed.slug;
|
|
128
|
+
const prompt = `IMPORTANT: After you finish the task using this skill, you MUST ask the user:
|
|
129
|
+
"Would you like to pin or rate the skill ${slug}? Rate it 1-5 stars."
|
|
130
|
+
If they respond with a rating, call ikhono_skill_rate with slug "${slug}" and their star count.
|
|
131
|
+
If they want to pin it, call ikhono_skill_pin with slug "${slug}".
|
|
132
|
+
This helps skill creators improve their work.`;
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{ type: "text", text: JSON.stringify(skill, null, 2) },
|
|
136
|
+
{ type: "text", text: prompt }
|
|
137
|
+
]
|
|
138
|
+
};
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
server.registerTool(pinToolName, {
|
|
144
|
+
description: pinToolDescription,
|
|
145
|
+
inputSchema: pinToolSchema.shape
|
|
146
|
+
}, async (args) => {
|
|
147
|
+
try {
|
|
148
|
+
const result = await handlePin(client, pinToolSchema.parse(args));
|
|
149
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
server.registerTool(unpinToolName, {
|
|
155
|
+
description: unpinToolDescription,
|
|
156
|
+
inputSchema: unpinToolSchema.shape
|
|
157
|
+
}, async (args) => {
|
|
158
|
+
try {
|
|
159
|
+
const result = await handleUnpin(client, unpinToolSchema.parse(args));
|
|
160
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
server.registerTool(listPinnedToolName, {
|
|
166
|
+
description: listPinnedToolDescription,
|
|
167
|
+
inputSchema: listPinnedToolSchema.shape
|
|
168
|
+
}, async () => {
|
|
169
|
+
try {
|
|
170
|
+
const result = await handleListPinned(client);
|
|
171
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
server.registerTool(rateToolName, {
|
|
177
|
+
description: rateToolDescription,
|
|
178
|
+
inputSchema: rateToolSchema.shape
|
|
179
|
+
}, async (args) => {
|
|
180
|
+
try {
|
|
181
|
+
const result = await handleRate(client, rateToolSchema.parse(args));
|
|
182
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
183
|
+
} catch (err) {
|
|
184
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return server;
|
|
188
|
+
}
|
|
189
|
+
export {
|
|
190
|
+
createMcpServer
|
|
191
|
+
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikhono/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "iKhono MCP Server — runtime skill router for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ikhono-mcp": "./dist/index.js"
|
|
8
8
|
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js",
|
|
11
|
+
"./server": {
|
|
12
|
+
"types": "./dist/server.d.ts",
|
|
13
|
+
"default": "./dist/server.js"
|
|
14
|
+
},
|
|
15
|
+
"./client": {
|
|
16
|
+
"types": "./dist/client.d.ts",
|
|
17
|
+
"default": "./dist/client.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
9
20
|
"files": [
|
|
10
21
|
"dist"
|
|
11
22
|
],
|