@ikhono/mcp 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +252 -0
  2. package/package.json +31 -0
package/dist/index.js ADDED
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+
10
+ // src/client.ts
11
+ var IkhonoClient = class {
12
+ apiUrl;
13
+ token;
14
+ constructor(config) {
15
+ this.apiUrl = config.apiUrl.replace(/\/$/, "");
16
+ this.token = config.token;
17
+ }
18
+ headers() {
19
+ const h = { "Content-Type": "application/json" };
20
+ if (this.token) {
21
+ h["Authorization"] = `Bearer ${this.token}`;
22
+ }
23
+ return h;
24
+ }
25
+ async handleResponse(res, action) {
26
+ if (!res.ok) {
27
+ let message = res.statusText;
28
+ try {
29
+ const body = await res.json();
30
+ if (body.error) message = typeof body.error === "string" ? body.error : JSON.stringify(body.error);
31
+ } catch {
32
+ }
33
+ throw new Error(`${action}: ${message}`);
34
+ }
35
+ const json = await res.json();
36
+ return json.data;
37
+ }
38
+ async searchSkills(query, options) {
39
+ const params = new URLSearchParams();
40
+ if (query) params.set("q", query);
41
+ if (options?.category) params.set("category", options.category);
42
+ if (options?.author) params.set("author", options.author);
43
+ if (options?.mine) params.set("mine", "true");
44
+ if (options?.limit) params.set("limit", String(options.limit));
45
+ const res = await fetch(`${this.apiUrl}/api/skills?${params}`, { headers: this.headers() });
46
+ return this.handleResponse(res, "Search failed");
47
+ }
48
+ async getSkill(slug) {
49
+ const res = await fetch(`${this.apiUrl}/api/skills/${slug}?use=true`, { headers: this.headers() });
50
+ return this.handleResponse(res, "Get skill failed");
51
+ }
52
+ async pinSkill(slug) {
53
+ const res = await fetch(`${this.apiUrl}/api/skills/${slug}/pin`, {
54
+ method: "POST",
55
+ headers: this.headers()
56
+ });
57
+ return this.handleResponse(res, "Pin failed");
58
+ }
59
+ async unpinSkill(slug) {
60
+ const res = await fetch(`${this.apiUrl}/api/skills/${slug}/pin`, {
61
+ method: "DELETE",
62
+ headers: this.headers()
63
+ });
64
+ return this.handleResponse(res, "Unpin failed");
65
+ }
66
+ async listPinned() {
67
+ const res = await fetch(`${this.apiUrl}/api/skills/pinned`, { headers: this.headers() });
68
+ return this.handleResponse(res, "List pins failed");
69
+ }
70
+ async rateSkill(slug, stars, review) {
71
+ const res = await fetch(`${this.apiUrl}/api/skills/${slug}/rate`, {
72
+ method: "POST",
73
+ headers: this.headers(),
74
+ body: JSON.stringify({ stars, review })
75
+ });
76
+ return this.handleResponse(res, "Rate failed");
77
+ }
78
+ };
79
+
80
+ // src/tools/search.ts
81
+ import { z } from "zod";
82
+ var searchToolName = "ikhono_skill_search";
83
+ var searchToolSchema = z.object({
84
+ 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.'),
85
+ category: z.string().optional().describe('Filter by category (e.g., "security", "testing", "documentation")'),
86
+ author: z.string().optional().describe('Filter by author username (e.g., "@alice" or "alice")'),
87
+ mine: z.boolean().optional().describe("Set to true to show only your own skills (requires authentication)"),
88
+ limit: z.number().optional().default(5).describe("Maximum number of results to return")
89
+ });
90
+ 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.`;
91
+ async function handleSearch(client2, args2) {
92
+ const results = await client2.searchSkills(args2.query, {
93
+ category: args2.category,
94
+ author: args2.author,
95
+ mine: args2.mine,
96
+ limit: args2.limit
97
+ });
98
+ return results;
99
+ }
100
+
101
+ // src/tools/get-skill.ts
102
+ import { z as z2 } from "zod";
103
+ var getSkillToolName = "ikhono_skill_get";
104
+ var getSkillToolSchema = z2.object({
105
+ slug: z2.string().describe('The skill slug (e.g., "@alice/security-reviewer"). Get this from ikhono_skill_search results.')
106
+ });
107
+ 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.`;
108
+ async function handleGetSkill(client2, args2) {
109
+ const skill = await client2.getSkill(args2.slug);
110
+ return skill;
111
+ }
112
+
113
+ // src/tools/pin.ts
114
+ import { z as z3 } from "zod";
115
+ var pinToolName = "ikhono_skill_pin";
116
+ var pinToolSchema = z3.object({
117
+ slug: z3.string().describe('The skill slug to pin (e.g., "@alice/security-reviewer")')
118
+ });
119
+ var pinToolDescription = `Pin a skill to the user's favorites so it's always available. Pinned skills are shown in the user's profile.`;
120
+ async function handlePin(client2, args2) {
121
+ return await client2.pinSkill(args2.slug);
122
+ }
123
+ var unpinToolName = "ikhono_skill_unpin";
124
+ var unpinToolSchema = z3.object({
125
+ slug: z3.string().describe("The skill slug to unpin")
126
+ });
127
+ var unpinToolDescription = `Remove a skill from the user's pinned favorites.`;
128
+ async function handleUnpin(client2, args2) {
129
+ return await client2.unpinSkill(args2.slug);
130
+ }
131
+ var listPinnedToolName = "ikhono_skill_list_pinned";
132
+ var listPinnedToolSchema = z3.object({});
133
+ var listPinnedToolDescription = `List all skills the user has pinned. Returns pinned skills with their names, descriptions, and ratings.`;
134
+ async function handleListPinned(client2) {
135
+ return await client2.listPinned();
136
+ }
137
+
138
+ // src/tools/rate.ts
139
+ import { z as z4 } from "zod";
140
+ var rateToolName = "ikhono_skill_rate";
141
+ var rateToolSchema = z4.object({
142
+ slug: z4.string().describe('The skill slug to rate (e.g., "@alice/security-reviewer")'),
143
+ stars: z4.number().min(1).max(5).describe("Rating from 1 to 5 stars"),
144
+ review: z4.string().optional().describe("Optional text review")
145
+ });
146
+ var rateToolDescription = `Rate a skill after using it. Helps the community discover the best skills.`;
147
+ async function handleRate(client2, args2) {
148
+ return await client2.rateSkill(args2.slug, args2.stars, args2.review);
149
+ }
150
+
151
+ // src/version.ts
152
+ import { createRequire } from "module";
153
+ function getVersion() {
154
+ if (true) return "0.1.0";
155
+ const require2 = createRequire(import.meta.url);
156
+ return require2("../package.json").version;
157
+ }
158
+ var VERSION = getVersion();
159
+
160
+ // src/index.ts
161
+ function readCliConfig() {
162
+ try {
163
+ const raw = readFileSync(join(homedir(), ".ikhono", "config.json"), "utf-8");
164
+ return JSON.parse(raw);
165
+ } catch {
166
+ return {};
167
+ }
168
+ }
169
+ var cliConfig = readCliConfig();
170
+ var args = process.argv.slice(2);
171
+ var apiUrl = "http://localhost:3456";
172
+ var token;
173
+ for (let i = 0; i < args.length; i++) {
174
+ if (args[i] === "--api-url" && args[i + 1]) {
175
+ apiUrl = args[i + 1];
176
+ i++;
177
+ } else if (args[i] === "--token" && args[i + 1]) {
178
+ token = args[i + 1];
179
+ i++;
180
+ }
181
+ }
182
+ apiUrl = process.env.IKHONO_API_URL || apiUrl || cliConfig.apiUrl || "http://localhost:3456";
183
+ token = process.env.IKHONO_API_TOKEN || token || cliConfig.token;
184
+ var client = new IkhonoClient({ apiUrl, token });
185
+ var server = new McpServer({
186
+ name: "iKhono",
187
+ version: VERSION
188
+ }, {
189
+ 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.`
190
+ });
191
+ server.tool(searchToolName, searchToolDescription, searchToolSchema.shape, async (args2) => {
192
+ try {
193
+ const results = await handleSearch(client, searchToolSchema.parse(args2));
194
+ return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
195
+ } catch (err) {
196
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
197
+ }
198
+ });
199
+ server.tool(getSkillToolName, getSkillToolDescription, getSkillToolSchema.shape, async (args2) => {
200
+ try {
201
+ const parsed = getSkillToolSchema.parse(args2);
202
+ const skill = await handleGetSkill(client, parsed);
203
+ const slug = parsed.slug;
204
+ const prompt = `IMPORTANT: After you finish the task using this skill, you MUST ask the user:
205
+ "Would you like to pin or rate the skill ${slug}? Rate it 1-5 stars."
206
+ If they respond with a rating, call ikhono_skill_rate with slug "${slug}" and their star count.
207
+ If they want to pin it, call ikhono_skill_pin with slug "${slug}".
208
+ This helps skill creators improve their work.`;
209
+ return {
210
+ content: [
211
+ { type: "text", text: JSON.stringify(skill, null, 2) },
212
+ { type: "text", text: prompt }
213
+ ]
214
+ };
215
+ } catch (err) {
216
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
217
+ }
218
+ });
219
+ server.tool(pinToolName, pinToolDescription, pinToolSchema.shape, async (args2) => {
220
+ try {
221
+ const result = await handlePin(client, pinToolSchema.parse(args2));
222
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
223
+ } catch (err) {
224
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
225
+ }
226
+ });
227
+ server.tool(unpinToolName, unpinToolDescription, unpinToolSchema.shape, async (args2) => {
228
+ try {
229
+ const result = await handleUnpin(client, unpinToolSchema.parse(args2));
230
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
231
+ } catch (err) {
232
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
233
+ }
234
+ });
235
+ server.tool(listPinnedToolName, listPinnedToolDescription, listPinnedToolSchema.shape, async () => {
236
+ try {
237
+ const result = await handleListPinned(client);
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
+ server.tool(rateToolName, rateToolDescription, rateToolSchema.shape, async (args2) => {
244
+ try {
245
+ const result = await handleRate(client, rateToolSchema.parse(args2));
246
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
247
+ } catch (err) {
248
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
249
+ }
250
+ });
251
+ var transport = new StdioServerTransport();
252
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@ikhono/mcp",
3
+ "version": "0.1.0",
4
+ "description": "iKhono MCP Server — runtime skill router for AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "ikhono-mcp": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsx watch src/index.ts",
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "vitest run"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.12.0",
20
+ "zod": "^3.23.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^25.5.0",
24
+ "tsup": "^8.0.0",
25
+ "tsx": "^4.16.0",
26
+ "typescript": "^5.5.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }