skillsmp-mcp-lite 1.0.1 → 1.0.3

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 CHANGED
@@ -102,6 +102,17 @@ AI semantic search for skills using natural language.
102
102
  |-----------|------|----------|-------------|
103
103
  | `query` | string | Yes | Natural language description |
104
104
 
105
+ ### `skillsmp_install_and_read_skill`
106
+
107
+ Install a skill from GitHub and immediately read its content.
108
+
109
+ | Parameter | Type | Required | Description |
110
+ |-----------|------|----------|-------------|
111
+ | `repo` | string | Yes | GitHub repository in 'owner/repo' format |
112
+ | `skillName` | string | Yes | Name of the skill to read after installation |
113
+ | `global` | boolean | No | Install globally to ~/.claude/skills/ (default: false) |
114
+ | `universal` | boolean | No | Install to .agent/skills/ for universal usage (default: false) |
115
+
105
116
  ## Usage Examples
106
117
 
107
118
  Ask your AI assistant:
@@ -109,6 +120,24 @@ Ask your AI assistant:
109
120
  - "Search for PDF manipulation skills"
110
121
  - "Find skills for building a web scraper"
111
122
  - "What skills can help me with SEO optimization?"
123
+ - "Install and load the python-code-review skill from existential-birds/beagle"
124
+
125
+ ## AGENTS.md Integration
126
+
127
+ To enable automatic skill discovery, copy the content from [`AGENTS.example.md`](./AGENTS.example.md) and paste it at the top of your `AGENTS.md` file.
128
+
129
+ ### How It Works
130
+
131
+ 1. AI receives a complex task
132
+ 2. AI searches SkillsMP for relevant skills using `skillsmp_search_skills`
133
+ 3. If keyword search is insufficient, AI tries `skillsmp_ai_search_skills`
134
+ 4. If a relevant skill is found, AI installs and reads it with `skillsmp_install_and_read_skill`
135
+ 5. AI follows the skill's instructions to complete the task
136
+
137
+ ### Search Tips
138
+
139
+ - **Keyword search**: Keep queries short (1-3 words). Example: `"code review"`, `"typescript"`, `"pdf"`
140
+ - **Semantic search**: Use natural language. Example: `"how to build a landing page with React"`
112
141
 
113
142
  ## License
114
143
 
package/dist/api.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * SkillsMP API Client and Types
3
3
  */
4
4
  export declare const SKILLSMP_API_BASE = "https://skillsmp.com/api/v1";
5
+ export declare const API_TIMEOUT_MS = 30000;
5
6
  export interface Skill {
6
7
  id: string;
7
8
  name: string;
package/dist/api.js CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
  // API Configuration
5
5
  export const SKILLSMP_API_BASE = "https://skillsmp.com/api/v1";
6
+ export const API_TIMEOUT_MS = 30_000;
6
7
  // API Client
7
8
  export async function makeApiRequest(endpoint, apiKey, params) {
8
9
  const url = new URL(`${SKILLSMP_API_BASE}/${endpoint}`);
@@ -13,13 +14,22 @@ export async function makeApiRequest(endpoint, apiKey, params) {
13
14
  }
14
15
  });
15
16
  }
16
- const response = await fetch(url.toString(), {
17
- method: "GET",
18
- headers: {
19
- "Authorization": `Bearer ${apiKey}`,
20
- "Content-Type": "application/json"
21
- }
22
- });
17
+ const controller = new AbortController();
18
+ const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
19
+ let response;
20
+ try {
21
+ response = await fetch(url.toString(), {
22
+ method: "GET",
23
+ headers: {
24
+ "Authorization": `Bearer ${apiKey}`,
25
+ "Content-Type": "application/json"
26
+ },
27
+ signal: controller.signal
28
+ });
29
+ }
30
+ finally {
31
+ clearTimeout(timeoutId);
32
+ }
23
33
  if (!response.ok) {
24
34
  const errorData = await response.json().catch(() => ({}));
25
35
  throw new Error(errorData.error?.message ||
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { createRequire } from "module";
4
5
  import { registerSkillsTools } from "./tools/skills.js";
6
+ const require = createRequire(import.meta.url);
7
+ const { version } = require("../package.json");
5
8
  /**
6
9
  * SkillsMP MCP Server
7
10
  *
@@ -20,7 +23,7 @@ if (!API_KEY) {
20
23
  // Initialize MCP Server
21
24
  const server = new McpServer({
22
25
  name: "skillsmp-mcp-server",
23
- version: "1.0.0"
26
+ version: version
24
27
  });
25
28
  // Register skills search tools
26
29
  registerSkillsTools(server, API_KEY);
package/dist/schemas.d.ts CHANGED
@@ -31,3 +31,20 @@ export declare const AISearchSchema: z.ZodObject<{
31
31
  query: string;
32
32
  }>;
33
33
  export type AISearchInput = z.infer<typeof AISearchSchema>;
34
+ export declare const InstallAndReadSchema: z.ZodObject<{
35
+ repo: z.ZodString;
36
+ skillName: z.ZodString;
37
+ global: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
38
+ universal: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
39
+ }, "strict", z.ZodTypeAny, {
40
+ repo: string;
41
+ skillName: string;
42
+ global: boolean;
43
+ universal: boolean;
44
+ }, {
45
+ repo: string;
46
+ skillName: string;
47
+ global?: boolean | undefined;
48
+ universal?: boolean | undefined;
49
+ }>;
50
+ export type InstallAndReadInput = z.infer<typeof InstallAndReadSchema>;
package/dist/schemas.js CHANGED
@@ -36,3 +36,21 @@ export const AISearchSchema = z.object({
36
36
  .max(500, "Query must not exceed 500 characters")
37
37
  .describe("Natural language description of what you want to accomplish")
38
38
  }).strict();
39
+ // Install and Read Skill Schema
40
+ export const InstallAndReadSchema = z.object({
41
+ repo: z.string()
42
+ .min(1, "Repository is required")
43
+ .regex(/^[^/]+\/[^/]+$/, "Repository must be in 'owner/repo' format")
44
+ .describe("GitHub repository in 'owner/repo' format (e.g., 'existential-birds/beagle')"),
45
+ skillName: z.string()
46
+ .min(1, "Skill name is required")
47
+ .describe("Name of the skill to read after installation"),
48
+ global: z.boolean()
49
+ .optional()
50
+ .default(false)
51
+ .describe("Install globally to ~/.claude/skills/ (default: false, installs to project)"),
52
+ universal: z.boolean()
53
+ .optional()
54
+ .default(false)
55
+ .describe("Install to .agent/skills/ for universal AGENTS.md usage (default: false)")
56
+ }).strict();
@@ -1,5 +1,33 @@
1
+ import { execFile } from "child_process";
2
+ import { promisify } from "util";
1
3
  import { makeApiRequest, handleApiError, validateSearchResponse, validateAISearchResponse } from "../api.js";
2
- import { KeywordSearchSchema, AISearchSchema } from "../schemas.js";
4
+ import { KeywordSearchSchema, AISearchSchema, InstallAndReadSchema } from "../schemas.js";
5
+ const execFileAsync = promisify(execFile);
6
+ // Timeout constants (in milliseconds)
7
+ const TIMEOUTS = {
8
+ READ: 30_000,
9
+ INSTALL: 120_000
10
+ };
11
+ /**
12
+ * Safely extract error message from unknown error type
13
+ */
14
+ function getErrorDetails(error) {
15
+ if (error instanceof Error) {
16
+ const execError = error;
17
+ return {
18
+ message: execError.message,
19
+ stderr: execError.stderr
20
+ };
21
+ }
22
+ if (typeof error === 'object' && error !== null) {
23
+ const e = error;
24
+ return {
25
+ message: String(e.message || 'Unknown error'),
26
+ stderr: typeof e.stderr === 'string' ? e.stderr : undefined
27
+ };
28
+ }
29
+ return { message: String(error) };
30
+ }
3
31
  /**
4
32
  * Register SkillsMP tools on the MCP server
5
33
  */
@@ -144,6 +172,111 @@ Examples:
144
172
  };
145
173
  }
146
174
  });
175
+ // Tool 3: Install and Read Skill
176
+ server.registerTool("skillsmp_install_and_read_skill", {
177
+ title: "Install and Read Skill",
178
+ description: `Install a skill from GitHub and immediately read its content.
179
+
180
+ This tool first checks if the skill is already installed locally. If found, it skips installation and directly reads the content (faster). If not found, it installs from GitHub first.
181
+
182
+ **IMPORTANT**: Use this to quickly load skill instructions without manual steps.
183
+
184
+ Args:
185
+ - repo (string, required): GitHub repository in 'owner/repo' format
186
+ - skillName (string, required): Name of the skill to read after installation
187
+ - global (boolean, optional): Install globally to ~/.claude/skills/ (default: false)
188
+ - universal (boolean, optional): Install to .agent/skills/ for universal usage (default: false)
189
+
190
+ Returns:
191
+ The full content of the skill's instructions (SKILL.md).
192
+
193
+ Examples:
194
+ - repo: "existential-birds/beagle", skillName: "python-code-review"
195
+ - repo: "LA3D/skillhelper", skillName: "code-reviewer"`,
196
+ inputSchema: InstallAndReadSchema,
197
+ annotations: {
198
+ readOnlyHint: false,
199
+ destructiveHint: false,
200
+ idempotentHint: true,
201
+ openWorldHint: true
202
+ }
203
+ }, async (params) => {
204
+ try {
205
+ const readArgs = ["-y", "openskills", "read", params.skillName];
206
+ // Step 1: Check if skill already exists locally
207
+ try {
208
+ const { stdout } = await execFileAsync("npx", readArgs, { timeout: TIMEOUTS.READ });
209
+ if (stdout && !stdout.includes("not found") && !stdout.includes("Error")) {
210
+ const output = formatInstallAndReadResponse(params.repo, params.skillName, stdout, true);
211
+ return {
212
+ content: [{ type: "text", text: output }],
213
+ structuredContent: {
214
+ repo: params.repo,
215
+ skillName: params.skillName,
216
+ skillContent: stdout,
217
+ wasAlreadyInstalled: true
218
+ }
219
+ };
220
+ }
221
+ }
222
+ catch {
223
+ // Skill not found locally, proceed with installation
224
+ }
225
+ // Step 2: Install if not exists
226
+ const installArgs = ["-y", "openskills", "install", params.repo];
227
+ if (params.global)
228
+ installArgs.push("--global");
229
+ if (params.universal)
230
+ installArgs.push("--universal");
231
+ let installOutput;
232
+ try {
233
+ const { stdout, stderr } = await execFileAsync("npx", installArgs, { timeout: TIMEOUTS.INSTALL });
234
+ installOutput = stdout || stderr;
235
+ }
236
+ catch (installError) {
237
+ const errorDetails = getErrorDetails(installError);
238
+ return {
239
+ content: [{
240
+ type: "text",
241
+ text: `❌ **Installation Failed**\n\nRepository: ${params.repo}\n\nError:\n${errorDetails.stderr || errorDetails.message}`
242
+ }]
243
+ };
244
+ }
245
+ // Step 3: Read the skill after installation
246
+ let readOutput;
247
+ try {
248
+ const { stdout, stderr } = await execFileAsync("npx", readArgs, { timeout: TIMEOUTS.READ });
249
+ readOutput = stdout || stderr;
250
+ }
251
+ catch (readError) {
252
+ const errorDetails = getErrorDetails(readError);
253
+ return {
254
+ content: [{
255
+ type: "text",
256
+ text: `✅ **Installation Succeeded** but **Read Failed**\n\nRepository: ${params.repo}\nSkill: ${params.skillName}\n\n**Install Output:**\n${installOutput}\n\n**Read Error:**\n${errorDetails.stderr || errorDetails.message}\n\n💡 **Tip**: Check if the skill name is correct. Use \`npx openskills list\` to see installed skills.`
257
+ }]
258
+ };
259
+ }
260
+ const output = formatInstallAndReadResponse(params.repo, params.skillName, readOutput, false);
261
+ return {
262
+ content: [{ type: "text", text: output }],
263
+ structuredContent: {
264
+ repo: params.repo,
265
+ skillName: params.skillName,
266
+ skillContent: readOutput,
267
+ wasAlreadyInstalled: false
268
+ }
269
+ };
270
+ }
271
+ catch (error) {
272
+ return {
273
+ content: [{
274
+ type: "text",
275
+ text: `❌ **Error**: ${error instanceof Error ? error.message : "An unexpected error occurred"}`
276
+ }]
277
+ };
278
+ }
279
+ });
147
280
  }
148
281
  /**
149
282
  * Render a single skill as markdown lines
@@ -205,6 +338,24 @@ function formatAISearchResponse(skills, query) {
205
338
  ""
206
339
  ];
207
340
  skills.forEach((skill, i) => lines.push(...renderSkill(skill, i)));
208
- lines.push("💡 **Tip**: Use `npx openskills read <skill-name>` to load a skill's detailed instructions.");
341
+ lines.push("💡 **Tip**: Use `skillsmp_install_and_read_skill` to install and load a skill's instructions.");
342
+ return lines.join("\n");
343
+ }
344
+ /**
345
+ * Format install and read response as markdown
346
+ */
347
+ function formatInstallAndReadResponse(repo, skillName, skillContent, wasAlreadyInstalled) {
348
+ const statusIcon = wasAlreadyInstalled ? "📖" : "📦";
349
+ const statusText = wasAlreadyInstalled ? "Skill Loaded (already installed)" : "Skill Installed & Loaded";
350
+ const lines = [
351
+ `# ${statusIcon} ${statusText}: ${skillName}`,
352
+ "",
353
+ `**Repository**: ${repo}`,
354
+ wasAlreadyInstalled ? "**Status**: ⚡ Loaded from local cache (skipped installation)" : "**Status**: ✅ Freshly installed",
355
+ "",
356
+ "---",
357
+ "",
358
+ skillContent
359
+ ];
209
360
  return lines.join("\n");
210
361
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillsmp-mcp-lite",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Lightweight MCP server for searching AI skills from SkillsMP before starting any task",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",