clawskills-mcp 0.5.0 → 0.6.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 CHANGED
@@ -49,6 +49,9 @@ docker run --rm -i clawskills-mcp
49
49
  | `list_playbooks` | List all available cross-tool workflow playbooks |
50
50
  | `get_playbook` | Fetch a full playbook by slug |
51
51
  | `search_playbooks` | Full-text search across all workflow playbooks |
52
+ | `search_clawskills` | Unified search across both skills and playbooks |
53
+
54
+ For most user queries, start with `search_clawskills`. It biases toward playbooks for workflow-shaped searches like `closed won onboarding`, `zendesk jira escalation`, or `lead sync`.
52
55
 
53
56
  ### `list_skills`
54
57
 
@@ -92,6 +95,14 @@ No arguments. Returns all available playbooks.
92
95
 
93
96
  Returns matching excerpts (±3 lines of context) grouped by playbook, capped at 5 matches per playbook.
94
97
 
98
+ ### `search_clawskills`
99
+
100
+ | Argument | Type | Required | Description |
101
+ |----------|------|----------|-------------|
102
+ | `query` | string | yes | Search string, e.g. `"closed won onboarding"`, `"zendesk jira escalation"`, `"lead sync"` |
103
+
104
+ Returns combined results across skills and playbooks. Workflow-oriented queries rank playbooks ahead of generic skill matches.
105
+
95
106
  ---
96
107
 
97
108
  ## Configuration
package/dist/index.js CHANGED
@@ -148,9 +148,55 @@ function findSkill(skills, name) {
148
148
  function findPlaybook(playbooks, name) {
149
149
  return findSkill(playbooks, name);
150
150
  }
151
+ function parseFrontmatter(content) {
152
+ if (!content.startsWith("---\n"))
153
+ return {};
154
+ const end = content.indexOf("\n---\n", 4);
155
+ if (end === -1)
156
+ return {};
157
+ const block = content.slice(4, end);
158
+ const metadata = {};
159
+ let currentKey = null;
160
+ for (const rawLine of block.split("\n")) {
161
+ const line = rawLine.trimEnd();
162
+ const keyMatch = line.match(/^([a-zA-Z0-9_]+):\s*(.*)$/);
163
+ if (keyMatch) {
164
+ const [, key, value] = keyMatch;
165
+ if (value === "") {
166
+ metadata[key] = [];
167
+ currentKey = key;
168
+ }
169
+ else {
170
+ metadata[key] = value.trim();
171
+ currentKey = null;
172
+ }
173
+ continue;
174
+ }
175
+ const listMatch = line.match(/^\s*-\s+(.*)$/);
176
+ if (listMatch && currentKey) {
177
+ const current = metadata[currentKey];
178
+ if (Array.isArray(current)) {
179
+ current.push(listMatch[1].trim());
180
+ }
181
+ }
182
+ }
183
+ return metadata;
184
+ }
185
+ function getPlaybookMetadata(content) {
186
+ const frontmatter = parseFrontmatter(content);
187
+ const systems = Array.isArray(frontmatter.systems) ? frontmatter.systems : [];
188
+ const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : [];
189
+ return {
190
+ title: typeof frontmatter.title === "string" ? frontmatter.title : undefined,
191
+ systems,
192
+ tags,
193
+ triggerType: typeof frontmatter.trigger_type === "string" ? frontmatter.trigger_type : undefined,
194
+ };
195
+ }
151
196
  function searchDocuments(documents, query) {
152
197
  const results = [];
153
198
  const lowerQuery = query.toLowerCase();
199
+ const queryTerms = lowerQuery.split(/[^a-z0-9]+/).filter(Boolean);
154
200
  const CONTEXT_LINES = 3;
155
201
  const MAX_PER_SKILL = 5;
156
202
  for (const [name, content] of documents.entries()) {
@@ -158,7 +204,10 @@ function searchDocuments(documents, query) {
158
204
  const excerpts = [];
159
205
  const usedRanges = [];
160
206
  for (let i = 0; i < lines.length; i++) {
161
- if (!lines[i].toLowerCase().includes(lowerQuery))
207
+ const lowerLine = lines[i].toLowerCase();
208
+ const matchesExact = lowerLine.includes(lowerQuery);
209
+ const matchesTerm = queryTerms.some((term) => lowerLine.includes(term));
210
+ if (!matchesExact && !matchesTerm)
162
211
  continue;
163
212
  const start = Math.max(0, i - CONTEXT_LINES);
164
213
  const end = Math.min(lines.length - 1, i + CONTEXT_LINES);
@@ -184,6 +233,36 @@ function searchSkills(skills, query) {
184
233
  function searchPlaybooks(playbooks, query) {
185
234
  return searchDocuments(playbooks, query);
186
235
  }
236
+ function searchClawskills(skills, playbooks, query) {
237
+ const normalizedQuery = query.toLowerCase();
238
+ const queryTerms = normalizedQuery.split(/[^a-z0-9]+/).filter(Boolean);
239
+ const workflowHints = new Set([
240
+ "sync", "onboarding", "closed", "won", "handoff", "escalate",
241
+ "escalation", "workflow", "playbook", "incident", "notify",
242
+ ]);
243
+ const workflowShaped = queryTerms.some((term) => workflowHints.has(term));
244
+ const skillResults = searchSkills(skills, query).map((result) => ({
245
+ ...result,
246
+ contentType: "skill",
247
+ score: result.excerpts.length * 10 + (workflowShaped ? 0 : 5),
248
+ }));
249
+ const playbookResults = searchPlaybooks(playbooks, query).map((result) => {
250
+ const metadata = getPlaybookMetadata(playbooks.get(result.name));
251
+ const metadataText = [metadata.title, ...metadata.systems, ...metadata.tags, metadata.triggerType]
252
+ .filter(Boolean)
253
+ .join(" ")
254
+ .toLowerCase();
255
+ const metadataMatches = queryTerms.filter((term) => metadataText.includes(term)).length;
256
+ return {
257
+ ...result,
258
+ contentType: "playbook",
259
+ score: result.excerpts.length * 10 + metadataMatches * 5 + (workflowShaped ? 15 : 0),
260
+ };
261
+ });
262
+ return [...playbookResults, ...skillResults]
263
+ .sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
264
+ .slice(0, 12);
265
+ }
187
266
  // ---------------------------------------------------------------------------
188
267
  // Skill summary (first meaningful line)
189
268
  // ---------------------------------------------------------------------------
@@ -198,7 +277,7 @@ function skillSummary(content) {
198
277
  // ---------------------------------------------------------------------------
199
278
  // Exports for testing
200
279
  // ---------------------------------------------------------------------------
201
- export { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, skillSummary, };
280
+ export { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, searchClawskills, getPlaybookMetadata, skillSummary, };
202
281
  // ---------------------------------------------------------------------------
203
282
  // MCP Server setup
204
283
  // ---------------------------------------------------------------------------
@@ -269,6 +348,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
269
348
  required: ["query"],
270
349
  },
271
350
  },
351
+ {
352
+ name: "search_clawskills",
353
+ description: "Search across both skills and playbooks. For workflow-shaped queries, playbooks are ranked ahead of generic skill matches.",
354
+ inputSchema: {
355
+ type: "object",
356
+ properties: {
357
+ query: { type: "string", description: "Search query, e.g. 'closed won onboarding', 'zendesk jira escalation', 'lead sync'" },
358
+ },
359
+ required: ["query"],
360
+ },
361
+ },
272
362
  ],
273
363
  }));
274
364
  // Call tools
@@ -276,6 +366,7 @@ const GetSkillSchema = z.object({ name: z.string(), section: z.string().optional
276
366
  const GetPlaybookSchema = z.object({ name: z.string() });
277
367
  const SearchSkillsSchema = z.object({ query: z.string() });
278
368
  const SearchPlaybooksSchema = z.object({ query: z.string() });
369
+ const SearchClawskillsSchema = z.object({ query: z.string() });
279
370
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
280
371
  const { name, arguments: args } = request.params;
281
372
  if (name === "list_skills") {
@@ -373,6 +464,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
373
464
  .join("\n\n---\n\n");
374
465
  return { content: [{ type: "text", text: `Playbook search results for "${query}":\n\n${output}` }] };
375
466
  }
467
+ if (name === "search_clawskills") {
468
+ const parsed = SearchClawskillsSchema.safeParse(args);
469
+ if (!parsed.success) {
470
+ return { content: [{ type: "text", text: `Invalid arguments: ${parsed.error.message}` }], isError: true };
471
+ }
472
+ const { query } = parsed.data;
473
+ const results = searchClawskills(skills, playbooks, query);
474
+ if (results.length === 0) {
475
+ return { content: [{ type: "text", text: `No results found for "${query}".` }] };
476
+ }
477
+ const output = results
478
+ .map(({ name: itemName, contentType, excerpts }) => {
479
+ const blocks = excerpts.map((e, i) => `--- match ${i + 1} ---\n${e}`).join("\n\n");
480
+ return `### [${contentType}] ${itemName}\n\n${blocks}`;
481
+ })
482
+ .join("\n\n---\n\n");
483
+ return { content: [{ type: "text", text: `Unified search results for "${query}":\n\n${output}` }] };
484
+ }
376
485
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
377
486
  });
378
487
  // ---------------------------------------------------------------------------
@@ -3,7 +3,7 @@ import * as fs from "fs";
3
3
  import * as os from "os";
4
4
  import * as path from "path";
5
5
  import { fileURLToPath } from "url";
6
- import { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, skillSummary, } from "./index.js";
6
+ import { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, searchClawskills, getPlaybookMetadata, skillSummary, } from "./index.js";
7
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
8
  const REAL_SKILLS_DIR = path.resolve(__dirname, "../../skills");
9
9
  const REAL_PLAYBOOKS_DIR = path.resolve(__dirname, "../../playbooks");
@@ -56,8 +56,8 @@ let tempPlaybooksDir;
56
56
  beforeAll(() => {
57
57
  tempDir = makeTempSkillsDir({ github: SAMPLE_CONTENT, salesforce: "# Salesforce\nCRM platform.\n## Overview\nSalesforce is a CRM." });
58
58
  tempPlaybooksDir = makeTempPlaybooksDir({
59
- "zendesk-jira": "# Zendesk -> Jira\n\nEscalate bugs from support into engineering.\n\n## Idempotency\n\nUse zendesk:{ticket_id}.",
60
- "hubspot-asana": "# HubSpot -> Asana\n\nCreate onboarding work from closed won deals.",
59
+ "zendesk-jira": "---\ntitle: Zendesk -> Jira\nsystems:\n - zendesk\n - jira\ntags:\n - support\n - escalation\ntrigger_type: webhook\n---\n# Zendesk -> Jira\n\nEscalate bugs from support into engineering.\n\n## Idempotency\n\nUse zendesk:{ticket_id}.",
60
+ "hubspot-asana": "---\ntitle: HubSpot -> Asana\nsystems:\n - hubspot\n - asana\ntags:\n - onboarding\n - handoff\ntrigger_type: webhook\n---\n# HubSpot -> Asana\n\nCreate onboarding work from closed won deals.",
61
61
  });
62
62
  });
63
63
  afterAll(() => {
@@ -229,6 +229,32 @@ describe("searchPlaybooks", () => {
229
229
  expect(results[0].name).toBe("zendesk-jira");
230
230
  });
231
231
  });
232
+ describe("getPlaybookMetadata", () => {
233
+ it("parses frontmatter metadata from playbooks", () => {
234
+ const playbooks = loadPlaybooks(tempPlaybooksDir);
235
+ const metadata = getPlaybookMetadata(playbooks.get("zendesk-jira"));
236
+ expect(metadata.systems).toContain("zendesk");
237
+ expect(metadata.tags).toContain("escalation");
238
+ expect(metadata.triggerType).toBe("webhook");
239
+ });
240
+ });
241
+ describe("searchClawskills", () => {
242
+ it("prefers playbooks for workflow-shaped queries", () => {
243
+ const skills = new Map([["hubspot", "HubSpot CRM API.\nUse for contacts and deals."]]);
244
+ const playbooks = loadPlaybooks(tempPlaybooksDir);
245
+ const results = searchClawskills(skills, playbooks, "closed won onboarding");
246
+ expect(results.length).toBeGreaterThan(0);
247
+ expect(results[0].contentType).toBe("playbook");
248
+ expect(results[0].name).toBe("hubspot-asana");
249
+ });
250
+ it("still returns skill hits for non-workflow queries", () => {
251
+ const skills = loadSkills(tempDir);
252
+ const playbooks = loadPlaybooks(tempPlaybooksDir);
253
+ const results = searchClawskills(skills, playbooks, "Personal Access Token");
254
+ expect(results.length).toBeGreaterThan(0);
255
+ expect(results.some((result) => result.contentType === "skill" && result.name === "github")).toBe(true);
256
+ });
257
+ });
232
258
  // ---------------------------------------------------------------------------
233
259
  // skillSummary
234
260
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawskills-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server exposing ClawSkills API integration skill docs to AI agents",
5
5
  "license": "MIT",
6
6
  "author": "ClawSkills Contributors",
@@ -1,3 +1,17 @@
1
+ ---
2
+ title: HubSpot Deal Won -> Asana Onboarding Kickoff
3
+ systems:
4
+ - hubspot
5
+ - asana
6
+ tags:
7
+ - crm
8
+ - onboarding
9
+ - handoff
10
+ - delivery
11
+ - revops
12
+ trigger_type: webhook
13
+ ---
14
+
1
15
  # HubSpot Deal Won -> Asana Onboarding Kickoff
2
16
 
3
17
  ## Goal
@@ -1,3 +1,17 @@
1
+ ---
2
+ title: Salesforce Lead -> HubSpot Contact Sync
3
+ systems:
4
+ - salesforce
5
+ - hubspot
6
+ tags:
7
+ - crm
8
+ - sync
9
+ - marketing
10
+ - lead
11
+ - revops
12
+ trigger_type: scheduled_sync
13
+ ---
14
+
1
15
  # Salesforce Lead -> HubSpot Contact Sync
2
16
 
3
17
  ## Goal
@@ -1,3 +1,17 @@
1
+ ---
2
+ title: Zendesk Ticket -> Jira Bug Escalation
3
+ systems:
4
+ - zendesk
5
+ - jira
6
+ tags:
7
+ - support
8
+ - engineering
9
+ - escalation
10
+ - incident
11
+ - handoff
12
+ trigger_type: webhook_or_incremental_sync
13
+ ---
14
+
1
15
  # Zendesk Ticket -> Jira Bug Escalation
2
16
 
3
17
  ## Goal
package/skills/ROADMAP.md CHANGED
@@ -19,7 +19,7 @@ This skill set enables end-to-end automation and integration across the most com
19
19
  | Layer | Status |
20
20
  |-------|--------|
21
21
  | **14 skill docs** — Monday.com, Salesforce, Jira, Dynamics 365, HubSpot, ServiceNow, Zendesk, Asana, GitHub, Figma, Slack, Stripe, Notion, Linear | ✅ Live |
22
- | **MCP server** — `clawskills-mcp` v0.4.0 on npm; `list_skills`, `get_skill`, `search_skills` | ✅ Live |
22
+ | **MCP server** — `clawskills-mcp` v0.5.0 on npm; `list_skills`, `get_skill`, `search_skills` | ✅ Live |
23
23
  | **CI pipeline** — build + test on every push/PR via `ci.yml` | ✅ Live |
24
24
  | **Release automation** — `release.yml` workflow_dispatch → bump, tag, npm publish via OIDC | ✅ Live |
25
25
  | **Test suite** — Vitest unit tests + real-skills validation (required sections, ≥5 KB, all 14 tools) | ✅ Live |
@@ -83,7 +83,7 @@ The repo now needs tighter alignment between the canonical `skills/` directory,
83
83
  | Channel | Status | Next action |
84
84
  |---------|--------|-------------|
85
85
  | GitHub (public) | Ready to flip | Make repo public |
86
- | npm (`clawskills-mcp`) | ✅ Live — v0.4.0 | Maintain via `release.yml` |
86
+ | npm (`clawskills-mcp`) | ✅ Live — v0.5.0 | Maintain via `release.yml` |
87
87
  | MCP registry listings | Not submitted | Submit to glama.ai, mcp.so, smithery.ai after public launch |
88
88
  | Docs website | Planned | Mintlify — fastest path to SEO-friendly browsable docs |
89
89
  | Lane A vs B | TBD | Revisit after first traction signals from public launch |