clawskills-mcp 0.3.6 → 0.5.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 +27 -4
- package/dist/index.js +115 -5
- package/dist/index.test.js +81 -2
- package/package.json +4 -2
- package/playbooks/INDEX.md +32 -0
- package/playbooks/hubspot-asana-onboarding.md +76 -0
- package/playbooks/salesforce-hubspot-lead-sync.md +78 -0
- package/playbooks/zendesk-jira-bug-escalation.md +79 -0
- package/skills/INDEX.md +1 -0
- package/skills/ROADMAP.md +40 -3
- package/skills/asana/skill.md +26 -0
- package/skills/hubspot/skill.md +61 -0
- package/skills/jira/skill.md +30 -0
- package/skills/salesforce/skill.md +45 -0
- package/skills/zendesk/skill.md +52 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
An MCP (Model Context Protocol) server that exposes [ClawSkills](https://github.com/Shanksg/clawskills) API integration skill docs to AI agents.
|
|
4
4
|
|
|
5
|
-
ClawSkills currently contains 14 structured Markdown skill docs
|
|
5
|
+
ClawSkills currently contains 14 structured Markdown skill docs plus workflow playbooks for cross-tool automations. Each skill tracks its own API version and `Last validated` date, while `clawskills-mcp` has its own npm version for the server itself.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -46,6 +46,9 @@ docker run --rm -i clawskills-mcp
|
|
|
46
46
|
| `list_skills` | List all available skill slugs with descriptions |
|
|
47
47
|
| `get_skill` | Fetch a full skill doc or a specific section |
|
|
48
48
|
| `search_skills` | Full-text search across all skill docs |
|
|
49
|
+
| `list_playbooks` | List all available cross-tool workflow playbooks |
|
|
50
|
+
| `get_playbook` | Fetch a full playbook by slug |
|
|
51
|
+
| `search_playbooks` | Full-text search across all workflow playbooks |
|
|
49
52
|
|
|
50
53
|
### `list_skills`
|
|
51
54
|
|
|
@@ -71,17 +74,36 @@ Examples:
|
|
|
71
74
|
|
|
72
75
|
Returns matching excerpts (±3 lines of context) grouped by skill, capped at 5 matches per skill.
|
|
73
76
|
|
|
77
|
+
### `list_playbooks`
|
|
78
|
+
|
|
79
|
+
No arguments. Returns all available playbooks.
|
|
80
|
+
|
|
81
|
+
### `get_playbook`
|
|
82
|
+
|
|
83
|
+
| Argument | Type | Required | Description |
|
|
84
|
+
|----------|------|----------|-------------|
|
|
85
|
+
| `name` | string | yes | Playbook slug: `zendesk-jira-bug-escalation`, `hubspot-asana-onboarding`, etc. Fuzzy matched. |
|
|
86
|
+
|
|
87
|
+
### `search_playbooks`
|
|
88
|
+
|
|
89
|
+
| Argument | Type | Required | Description |
|
|
90
|
+
|----------|------|----------|-------------|
|
|
91
|
+
| `query` | string | yes | Search string, e.g. `"idempotency"`, `"rollback"`, `"closed won"` |
|
|
92
|
+
|
|
93
|
+
Returns matching excerpts (±3 lines of context) grouped by playbook, capped at 5 matches per playbook.
|
|
94
|
+
|
|
74
95
|
---
|
|
75
96
|
|
|
76
97
|
## Configuration
|
|
77
98
|
|
|
78
|
-
Skills are bundled inside the npm package — no
|
|
99
|
+
Skills and playbooks are bundled inside the npm package — no extra configuration is needed for the standard `npx` setup.
|
|
79
100
|
|
|
80
|
-
`SKILLS_DIR`
|
|
101
|
+
`SKILLS_DIR` and `PLAYBOOKS_DIR` are available if you want to point the server at custom or local content directories:
|
|
81
102
|
|
|
82
103
|
| Env var | Default | Description |
|
|
83
104
|
|---------|---------|-------------|
|
|
84
105
|
| `SKILLS_DIR` | bundled `skills/` inside the package | Absolute path to a directory of `{slug}/skill.md` files |
|
|
106
|
+
| `PLAYBOOKS_DIR` | bundled `playbooks/` inside the package | Absolute path to a directory of `{slug}.md` playbook files |
|
|
85
107
|
|
|
86
108
|
```json
|
|
87
109
|
{
|
|
@@ -90,7 +112,8 @@ Skills are bundled inside the npm package — no `SKILLS_DIR` configuration need
|
|
|
90
112
|
"command": "npx",
|
|
91
113
|
"args": ["-y", "clawskills-mcp"],
|
|
92
114
|
"env": {
|
|
93
|
-
"SKILLS_DIR": "/path/to/your/skills"
|
|
115
|
+
"SKILLS_DIR": "/path/to/your/skills",
|
|
116
|
+
"PLAYBOOKS_DIR": "/path/to/your/playbooks"
|
|
94
117
|
}
|
|
95
118
|
}
|
|
96
119
|
}
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,19 @@ function resolveSkillsDir() {
|
|
|
25
25
|
}
|
|
26
26
|
return candidates[0];
|
|
27
27
|
}
|
|
28
|
+
function resolvePlaybooksDir() {
|
|
29
|
+
if (process.env.PLAYBOOKS_DIR)
|
|
30
|
+
return process.env.PLAYBOOKS_DIR;
|
|
31
|
+
const candidates = [
|
|
32
|
+
path.resolve(__dirname, "../../playbooks"),
|
|
33
|
+
path.resolve(__dirname, "../playbooks"),
|
|
34
|
+
];
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
if (fs.existsSync(candidate))
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
return candidates[0];
|
|
40
|
+
}
|
|
28
41
|
function resolvePackageVersion() {
|
|
29
42
|
const packageJsonPath = path.resolve(__dirname, "../package.json");
|
|
30
43
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
@@ -49,6 +62,23 @@ function loadSkills(skillsDir) {
|
|
|
49
62
|
process.stderr.write(`[clawskills-mcp] Loaded ${skills.size} skills: ${[...skills.keys()].join(", ")}\n`);
|
|
50
63
|
return skills;
|
|
51
64
|
}
|
|
65
|
+
function loadPlaybooks(playbooksDir) {
|
|
66
|
+
const playbooks = new Map();
|
|
67
|
+
if (!fs.existsSync(playbooksDir)) {
|
|
68
|
+
process.stderr.write(`[clawskills-mcp] Playbooks directory not found: ${playbooksDir}\n`);
|
|
69
|
+
return playbooks;
|
|
70
|
+
}
|
|
71
|
+
const entries = fs.readdirSync(playbooksDir, { withFileTypes: true });
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name === "INDEX.md")
|
|
74
|
+
continue;
|
|
75
|
+
const slug = entry.name.replace(/\.md$/, "");
|
|
76
|
+
const content = fs.readFileSync(path.join(playbooksDir, entry.name), "utf-8");
|
|
77
|
+
playbooks.set(slug, content);
|
|
78
|
+
}
|
|
79
|
+
process.stderr.write(`[clawskills-mcp] Loaded ${playbooks.size} playbooks: ${[...playbooks.keys()].join(", ")}\n`);
|
|
80
|
+
return playbooks;
|
|
81
|
+
}
|
|
52
82
|
// ---------------------------------------------------------------------------
|
|
53
83
|
// Section extraction
|
|
54
84
|
// ---------------------------------------------------------------------------
|
|
@@ -115,12 +145,15 @@ function findSkill(skills, name) {
|
|
|
115
145
|
}
|
|
116
146
|
return undefined;
|
|
117
147
|
}
|
|
118
|
-
function
|
|
148
|
+
function findPlaybook(playbooks, name) {
|
|
149
|
+
return findSkill(playbooks, name);
|
|
150
|
+
}
|
|
151
|
+
function searchDocuments(documents, query) {
|
|
119
152
|
const results = [];
|
|
120
153
|
const lowerQuery = query.toLowerCase();
|
|
121
154
|
const CONTEXT_LINES = 3;
|
|
122
155
|
const MAX_PER_SKILL = 5;
|
|
123
|
-
for (const [
|
|
156
|
+
for (const [name, content] of documents.entries()) {
|
|
124
157
|
const lines = content.split("\n");
|
|
125
158
|
const excerpts = [];
|
|
126
159
|
const usedRanges = [];
|
|
@@ -140,11 +173,17 @@ function searchSkills(skills, query) {
|
|
|
140
173
|
break;
|
|
141
174
|
}
|
|
142
175
|
if (excerpts.length > 0) {
|
|
143
|
-
results.push({
|
|
176
|
+
results.push({ name, excerpts });
|
|
144
177
|
}
|
|
145
178
|
}
|
|
146
179
|
return results;
|
|
147
180
|
}
|
|
181
|
+
function searchSkills(skills, query) {
|
|
182
|
+
return searchDocuments(skills, query);
|
|
183
|
+
}
|
|
184
|
+
function searchPlaybooks(playbooks, query) {
|
|
185
|
+
return searchDocuments(playbooks, query);
|
|
186
|
+
}
|
|
148
187
|
// ---------------------------------------------------------------------------
|
|
149
188
|
// Skill summary (first meaningful line)
|
|
150
189
|
// ---------------------------------------------------------------------------
|
|
@@ -159,13 +198,15 @@ function skillSummary(content) {
|
|
|
159
198
|
// ---------------------------------------------------------------------------
|
|
160
199
|
// Exports for testing
|
|
161
200
|
// ---------------------------------------------------------------------------
|
|
162
|
-
export { resolveSkillsDir, resolvePackageVersion, loadSkills, extractSection, findSkill, searchSkills, skillSummary };
|
|
201
|
+
export { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, skillSummary, };
|
|
163
202
|
// ---------------------------------------------------------------------------
|
|
164
203
|
// MCP Server setup
|
|
165
204
|
// ---------------------------------------------------------------------------
|
|
166
205
|
const SKILLS_DIR = resolveSkillsDir();
|
|
206
|
+
const PLAYBOOKS_DIR = resolvePlaybooksDir();
|
|
167
207
|
const PACKAGE_VERSION = resolvePackageVersion();
|
|
168
208
|
const skills = loadSkills(SKILLS_DIR);
|
|
209
|
+
const playbooks = loadPlaybooks(PLAYBOOKS_DIR);
|
|
169
210
|
const server = new Server({ name: "clawskills-mcp", version: PACKAGE_VERSION }, { capabilities: { tools: {} } });
|
|
170
211
|
// List tools
|
|
171
212
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -201,11 +242,40 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
201
242
|
required: ["query"],
|
|
202
243
|
},
|
|
203
244
|
},
|
|
245
|
+
{
|
|
246
|
+
name: "list_playbooks",
|
|
247
|
+
description: "List all available ClawSkills workflow playbooks.",
|
|
248
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "get_playbook",
|
|
252
|
+
description: "Retrieve a ClawSkills workflow playbook by name (slug).",
|
|
253
|
+
inputSchema: {
|
|
254
|
+
type: "object",
|
|
255
|
+
properties: {
|
|
256
|
+
name: { type: "string", description: "Playbook slug, e.g. 'zendesk-jira-bug-escalation'" },
|
|
257
|
+
},
|
|
258
|
+
required: ["name"],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: "search_playbooks",
|
|
263
|
+
description: "Search across workflow playbooks for a query string and return matching excerpts with context.",
|
|
264
|
+
inputSchema: {
|
|
265
|
+
type: "object",
|
|
266
|
+
properties: {
|
|
267
|
+
query: { type: "string", description: "Search query, e.g. 'idempotency', 'rollback', 'customer impact'" },
|
|
268
|
+
},
|
|
269
|
+
required: ["query"],
|
|
270
|
+
},
|
|
271
|
+
},
|
|
204
272
|
],
|
|
205
273
|
}));
|
|
206
274
|
// Call tools
|
|
207
275
|
const GetSkillSchema = z.object({ name: z.string(), section: z.string().optional() });
|
|
276
|
+
const GetPlaybookSchema = z.object({ name: z.string() });
|
|
208
277
|
const SearchSkillsSchema = z.object({ query: z.string() });
|
|
278
|
+
const SearchPlaybooksSchema = z.object({ query: z.string() });
|
|
209
279
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
210
280
|
const { name, arguments: args } = request.params;
|
|
211
281
|
if (name === "list_skills") {
|
|
@@ -218,6 +288,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
218
288
|
});
|
|
219
289
|
return { content: [{ type: "text", text: `Available skills (${skills.size}):\n\n${lines.join("\n")}` }] };
|
|
220
290
|
}
|
|
291
|
+
if (name === "list_playbooks") {
|
|
292
|
+
if (playbooks.size === 0) {
|
|
293
|
+
return { content: [{ type: "text", text: "No playbooks loaded. Check PLAYBOOKS_DIR." }] };
|
|
294
|
+
}
|
|
295
|
+
const lines = [...playbooks.entries()].map(([slug, content]) => `• **${slug}** — ${skillSummary(content)}`);
|
|
296
|
+
return { content: [{ type: "text", text: `Available playbooks (${playbooks.size}):\n\n${lines.join("\n")}` }] };
|
|
297
|
+
}
|
|
221
298
|
if (name === "get_skill") {
|
|
222
299
|
const parsed = GetSkillSchema.safeParse(args);
|
|
223
300
|
if (!parsed.success) {
|
|
@@ -256,13 +333,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
256
333
|
return { content: [{ type: "text", text: `No results found for "${query}".` }] };
|
|
257
334
|
}
|
|
258
335
|
const output = results
|
|
259
|
-
.map(({ skill, excerpts }) => {
|
|
336
|
+
.map(({ name: skill, excerpts }) => {
|
|
260
337
|
const blocks = excerpts.map((e, i) => `--- match ${i + 1} ---\n${e}`).join("\n\n");
|
|
261
338
|
return `### ${skill}\n\n${blocks}`;
|
|
262
339
|
})
|
|
263
340
|
.join("\n\n---\n\n");
|
|
264
341
|
return { content: [{ type: "text", text: `Search results for "${query}":\n\n${output}` }] };
|
|
265
342
|
}
|
|
343
|
+
if (name === "get_playbook") {
|
|
344
|
+
const parsed = GetPlaybookSchema.safeParse(args);
|
|
345
|
+
if (!parsed.success) {
|
|
346
|
+
return { content: [{ type: "text", text: `Invalid arguments: ${parsed.error.message}` }], isError: true };
|
|
347
|
+
}
|
|
348
|
+
const slug = findPlaybook(playbooks, parsed.data.name);
|
|
349
|
+
if (!slug) {
|
|
350
|
+
const available = [...playbooks.keys()].join(", ");
|
|
351
|
+
return {
|
|
352
|
+
content: [{ type: "text", text: `Playbook "${parsed.data.name}" not found. Available playbooks: ${available}` }],
|
|
353
|
+
isError: true,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return { content: [{ type: "text", text: playbooks.get(slug) }] };
|
|
357
|
+
}
|
|
358
|
+
if (name === "search_playbooks") {
|
|
359
|
+
const parsed = SearchPlaybooksSchema.safeParse(args);
|
|
360
|
+
if (!parsed.success) {
|
|
361
|
+
return { content: [{ type: "text", text: `Invalid arguments: ${parsed.error.message}` }], isError: true };
|
|
362
|
+
}
|
|
363
|
+
const { query } = parsed.data;
|
|
364
|
+
const results = searchPlaybooks(playbooks, query);
|
|
365
|
+
if (results.length === 0) {
|
|
366
|
+
return { content: [{ type: "text", text: `No playbook results found for "${query}".` }] };
|
|
367
|
+
}
|
|
368
|
+
const output = results
|
|
369
|
+
.map(({ name: playbook, excerpts }) => {
|
|
370
|
+
const blocks = excerpts.map((e, i) => `--- match ${i + 1} ---\n${e}`).join("\n\n");
|
|
371
|
+
return `### ${playbook}\n\n${blocks}`;
|
|
372
|
+
})
|
|
373
|
+
.join("\n\n---\n\n");
|
|
374
|
+
return { content: [{ type: "text", text: `Playbook search results for "${query}":\n\n${output}` }] };
|
|
375
|
+
}
|
|
266
376
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
267
377
|
});
|
|
268
378
|
// ---------------------------------------------------------------------------
|
package/dist/index.test.js
CHANGED
|
@@ -3,9 +3,10 @@ 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, resolvePackageVersion, loadSkills, extractSection, findSkill, searchSkills, skillSummary } from "./index.js";
|
|
6
|
+
import { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, 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
|
+
const REAL_PLAYBOOKS_DIR = path.resolve(__dirname, "../../playbooks");
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Helpers
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
@@ -18,6 +19,13 @@ function makeTempSkillsDir(skills) {
|
|
|
18
19
|
}
|
|
19
20
|
return dir;
|
|
20
21
|
}
|
|
22
|
+
function makeTempPlaybooksDir(playbooks) {
|
|
23
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clawskills-playbooks-test-"));
|
|
24
|
+
for (const [slug, content] of Object.entries(playbooks)) {
|
|
25
|
+
fs.writeFileSync(path.join(dir, `${slug}.md`), content, "utf-8");
|
|
26
|
+
}
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
21
29
|
const SAMPLE_CONTENT = `# GitHub Skill
|
|
22
30
|
|
|
23
31
|
A skill doc for GitHub API.
|
|
@@ -44,11 +52,17 @@ Scopes: repo, read:user, etc.
|
|
|
44
52
|
POST /repos/{owner}/{repo}/issues
|
|
45
53
|
`;
|
|
46
54
|
let tempDir;
|
|
55
|
+
let tempPlaybooksDir;
|
|
47
56
|
beforeAll(() => {
|
|
48
57
|
tempDir = makeTempSkillsDir({ github: SAMPLE_CONTENT, salesforce: "# Salesforce\nCRM platform.\n## Overview\nSalesforce is a CRM." });
|
|
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.",
|
|
61
|
+
});
|
|
49
62
|
});
|
|
50
63
|
afterAll(() => {
|
|
51
64
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
65
|
+
fs.rmSync(tempPlaybooksDir, { recursive: true, force: true });
|
|
52
66
|
});
|
|
53
67
|
// ---------------------------------------------------------------------------
|
|
54
68
|
// loadSkills
|
|
@@ -86,6 +100,12 @@ describe("resolveSkillsDir", () => {
|
|
|
86
100
|
expect(loadSkills(resolveSkillsDir()).has("stripe")).toBe(true);
|
|
87
101
|
});
|
|
88
102
|
});
|
|
103
|
+
describe("resolvePlaybooksDir", () => {
|
|
104
|
+
it("prefers the canonical repo-root playbooks directory", () => {
|
|
105
|
+
expect(path.basename(resolvePlaybooksDir())).toBe("playbooks");
|
|
106
|
+
expect(loadPlaybooks(resolvePlaybooksDir()).has("zendesk-jira-bug-escalation")).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
89
109
|
describe("resolvePackageVersion", () => {
|
|
90
110
|
it("matches package.json", () => {
|
|
91
111
|
const packageJsonPath = path.resolve(__dirname, "../package.json");
|
|
@@ -94,6 +114,20 @@ describe("resolvePackageVersion", () => {
|
|
|
94
114
|
});
|
|
95
115
|
});
|
|
96
116
|
// ---------------------------------------------------------------------------
|
|
117
|
+
// loadPlaybooks
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
describe("loadPlaybooks", () => {
|
|
120
|
+
it("loads markdown files from a directory", () => {
|
|
121
|
+
const playbooks = loadPlaybooks(tempPlaybooksDir);
|
|
122
|
+
expect(playbooks.size).toBe(2);
|
|
123
|
+
expect(playbooks.has("zendesk-jira")).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it("returns empty map for missing directory", () => {
|
|
126
|
+
const playbooks = loadPlaybooks("/nonexistent/playbooks/path");
|
|
127
|
+
expect(playbooks.size).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
97
131
|
// findSkill
|
|
98
132
|
// ---------------------------------------------------------------------------
|
|
99
133
|
describe("findSkill", () => {
|
|
@@ -119,6 +153,18 @@ describe("findSkill", () => {
|
|
|
119
153
|
expect(findSkill(skills, "zzznomatch")).toBeUndefined();
|
|
120
154
|
});
|
|
121
155
|
});
|
|
156
|
+
describe("findPlaybook", () => {
|
|
157
|
+
let playbooks;
|
|
158
|
+
beforeAll(() => {
|
|
159
|
+
playbooks = loadPlaybooks(tempPlaybooksDir);
|
|
160
|
+
});
|
|
161
|
+
it("returns key for exact match", () => {
|
|
162
|
+
expect(findPlaybook(playbooks, "zendesk-jira")).toBe("zendesk-jira");
|
|
163
|
+
});
|
|
164
|
+
it("matches normalized input", () => {
|
|
165
|
+
expect(findPlaybook(playbooks, "zendesk jira")).toBe("zendesk-jira");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
122
168
|
// ---------------------------------------------------------------------------
|
|
123
169
|
// extractSection
|
|
124
170
|
// ---------------------------------------------------------------------------
|
|
@@ -153,7 +199,7 @@ describe("searchSkills", () => {
|
|
|
153
199
|
it("returns results for a matching query", () => {
|
|
154
200
|
const results = searchSkills(skills, "Personal Access Token");
|
|
155
201
|
expect(results.length).toBeGreaterThan(0);
|
|
156
|
-
expect(results[0].
|
|
202
|
+
expect(results[0].name).toBe("github");
|
|
157
203
|
expect(results[0].excerpts.length).toBeGreaterThan(0);
|
|
158
204
|
});
|
|
159
205
|
it("is case-insensitive", () => {
|
|
@@ -172,6 +218,17 @@ describe("searchSkills", () => {
|
|
|
172
218
|
expect(results[0].excerpts.length).toBeLessThanOrEqual(5);
|
|
173
219
|
});
|
|
174
220
|
});
|
|
221
|
+
describe("searchPlaybooks", () => {
|
|
222
|
+
let playbooks;
|
|
223
|
+
beforeAll(() => {
|
|
224
|
+
playbooks = loadPlaybooks(tempPlaybooksDir);
|
|
225
|
+
});
|
|
226
|
+
it("returns results for a matching query", () => {
|
|
227
|
+
const results = searchPlaybooks(playbooks, "idempotency");
|
|
228
|
+
expect(results.length).toBeGreaterThan(0);
|
|
229
|
+
expect(results[0].name).toBe("zendesk-jira");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
175
232
|
// ---------------------------------------------------------------------------
|
|
176
233
|
// skillSummary
|
|
177
234
|
// ---------------------------------------------------------------------------
|
|
@@ -234,3 +291,25 @@ describe("real skills", () => {
|
|
|
234
291
|
}
|
|
235
292
|
});
|
|
236
293
|
});
|
|
294
|
+
describe("real playbooks", () => {
|
|
295
|
+
let playbooks;
|
|
296
|
+
beforeAll(() => {
|
|
297
|
+
playbooks = loadPlaybooks(REAL_PLAYBOOKS_DIR);
|
|
298
|
+
});
|
|
299
|
+
it("loads all expected playbooks", () => {
|
|
300
|
+
const knownPlaybooks = [
|
|
301
|
+
"hubspot-asana-onboarding",
|
|
302
|
+
"salesforce-hubspot-lead-sync",
|
|
303
|
+
"zendesk-jira-bug-escalation",
|
|
304
|
+
];
|
|
305
|
+
for (const slug of knownPlaybooks) {
|
|
306
|
+
expect(playbooks.has(slug), `Missing playbook: ${slug}`).toBe(true);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
it("every playbook has a non-empty summary line", () => {
|
|
310
|
+
for (const [slug, content] of playbooks.entries()) {
|
|
311
|
+
const summary = skillSummary(content);
|
|
312
|
+
expect(summary, `${slug}: empty summary`).not.toBe("");
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawskills-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server exposing ClawSkills API integration skill docs to AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ClawSkills Contributors",
|
|
@@ -30,12 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"dist/",
|
|
33
|
-
"skills/"
|
|
33
|
+
"skills/",
|
|
34
|
+
"playbooks/"
|
|
34
35
|
],
|
|
35
36
|
"scripts": {
|
|
36
37
|
"sync:skills": "node scripts/sync-skills.mjs",
|
|
37
38
|
"check:skills": "node scripts/check-skills.mjs",
|
|
38
39
|
"check:freshness": "node scripts/check-skills.mjs --freshness",
|
|
40
|
+
"check:docs": "node scripts/check-docs.mjs",
|
|
39
41
|
"prepack": "npm run sync:skills",
|
|
40
42
|
"build": "npm run sync:skills && tsc",
|
|
41
43
|
"start": "node dist/index.js",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Playbooks Index
|
|
2
|
+
|
|
3
|
+
Workflow playbooks are end-to-end guides for multi-system automations. They complement the per-tool `skill.md` files by defining the orchestration layer: trigger, source of truth, field mapping, idempotency, failure policy, and operational checks.
|
|
4
|
+
|
|
5
|
+
## Available playbooks
|
|
6
|
+
|
|
7
|
+
| Playbook | Systems | Primary use case |
|
|
8
|
+
|----------|---------|------------------|
|
|
9
|
+
| [Zendesk Ticket -> Jira Bug escalation](./zendesk-jira-bug-escalation.md) | Zendesk, Jira | Escalate support-confirmed defects into engineering workflow |
|
|
10
|
+
| [HubSpot Deal Won -> Asana onboarding kickoff](./hubspot-asana-onboarding.md) | HubSpot, Asana | Start implementation or onboarding work when a deal closes |
|
|
11
|
+
| [Salesforce Lead -> HubSpot Contact sync](./salesforce-hubspot-lead-sync.md) | Salesforce, HubSpot | Mirror new/updated leads into marketing automation without duplicates |
|
|
12
|
+
|
|
13
|
+
## Playbook template
|
|
14
|
+
|
|
15
|
+
Each playbook should define:
|
|
16
|
+
- Business trigger
|
|
17
|
+
- Systems involved
|
|
18
|
+
- Source of truth
|
|
19
|
+
- Sequence of operations
|
|
20
|
+
- Field mapping
|
|
21
|
+
- Idempotency and dedup keys
|
|
22
|
+
- Retry / partial-failure policy
|
|
23
|
+
- Reconciliation / rollback guidance
|
|
24
|
+
- Observability checks
|
|
25
|
+
|
|
26
|
+
## Next candidates
|
|
27
|
+
|
|
28
|
+
- Slack incident -> Jira issue
|
|
29
|
+
- GitHub PR -> Slack notification
|
|
30
|
+
- Zendesk -> Salesforce case sync
|
|
31
|
+
- Stripe payment failed -> HubSpot task
|
|
32
|
+
- Notion request -> Asana task
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# HubSpot Deal Won -> Asana Onboarding Kickoff
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Create onboarding or implementation work in Asana as soon as a HubSpot Deal is marked closed won.
|
|
6
|
+
|
|
7
|
+
## Systems involved
|
|
8
|
+
|
|
9
|
+
- HubSpot: sales pipeline, deal state, customer/account context
|
|
10
|
+
- Asana: delivery execution, onboarding tasks, project coordination
|
|
11
|
+
|
|
12
|
+
## Trigger
|
|
13
|
+
|
|
14
|
+
- HubSpot webhook `deal.propertyChange` where `dealstage` moves into the closed-won stage
|
|
15
|
+
|
|
16
|
+
## Source of truth
|
|
17
|
+
|
|
18
|
+
- Commercial state and customer/account metadata: HubSpot
|
|
19
|
+
- Delivery execution and task/project state: Asana
|
|
20
|
+
- Workflow linkage: integration database or HubSpot custom property storing the Asana `gid`
|
|
21
|
+
|
|
22
|
+
## Sequence
|
|
23
|
+
|
|
24
|
+
1. Receive the HubSpot webhook and verify it corresponds to the target pipeline/stage.
|
|
25
|
+
2. Fetch the Deal, associated Company, and primary Contact.
|
|
26
|
+
3. Check whether an Asana task/project already exists for the Deal ID.
|
|
27
|
+
4. Create the onboarding project or kickoff task in Asana.
|
|
28
|
+
5. Populate notes and custom fields with customer, owner, ARR, plan, and target launch details.
|
|
29
|
+
6. Persist the Asana `gid` back to HubSpot or your integration store.
|
|
30
|
+
7. Notify the implementation owner or CSM if required.
|
|
31
|
+
|
|
32
|
+
## Field mapping
|
|
33
|
+
|
|
34
|
+
| HubSpot | Asana |
|
|
35
|
+
|---------|-------|
|
|
36
|
+
| `dealId` | external mapping / custom field |
|
|
37
|
+
| `dealname` | task/project name |
|
|
38
|
+
| associated company name | notes / title suffix |
|
|
39
|
+
| deal owner | assignee |
|
|
40
|
+
| close date | kickoff anchor or due date |
|
|
41
|
+
| ARR / amount | custom field |
|
|
42
|
+
| onboarding tier | custom field / template choice |
|
|
43
|
+
|
|
44
|
+
## Idempotency
|
|
45
|
+
|
|
46
|
+
- Dedup key: HubSpot Deal ID
|
|
47
|
+
- Webhook delivery is at-least-once, so check your mapping store before creation
|
|
48
|
+
- If using project templates, ensure only one project is generated per Deal ID
|
|
49
|
+
|
|
50
|
+
## Retry and partial failure policy
|
|
51
|
+
|
|
52
|
+
- Retry transient Asana failures with bounded backoff
|
|
53
|
+
- If Asana create succeeds but write-back to HubSpot fails, recover by searching the mapping store on the next run
|
|
54
|
+
- Validation errors should move the record into a manual review queue with the Deal ID and failing field
|
|
55
|
+
|
|
56
|
+
## Reconciliation
|
|
57
|
+
|
|
58
|
+
- Daily job: compare all closed-won deals in the last 30 days against Asana kickoff artifacts
|
|
59
|
+
- Backfill missing Asana `gid` values into HubSpot
|
|
60
|
+
- Flag onboarding projects with no matching active deal
|
|
61
|
+
|
|
62
|
+
## Rollback guidance
|
|
63
|
+
|
|
64
|
+
- If a Deal reopens or is marked closed-lost shortly after creation, archive or cancel the Asana project/task rather than deleting it
|
|
65
|
+
- Preserve the linkage so repeated stage churn does not create duplicates
|
|
66
|
+
|
|
67
|
+
## Observability
|
|
68
|
+
|
|
69
|
+
- Count created onboarding projects/tasks per day
|
|
70
|
+
- Alert on webhook processing failures and duplicate prevention hits
|
|
71
|
+
- Track time from closed won to Asana kickoff creation
|
|
72
|
+
|
|
73
|
+
## Related skills
|
|
74
|
+
|
|
75
|
+
- [`skills/hubspot/skill.md`](../skills/hubspot/skill.md)
|
|
76
|
+
- [`skills/asana/skill.md`](../skills/asana/skill.md)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Salesforce Lead -> HubSpot Contact Sync
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Mirror new or updated Salesforce Leads into HubSpot Contacts so marketing and lifecycle workflows stay current without creating duplicates.
|
|
6
|
+
|
|
7
|
+
## Systems involved
|
|
8
|
+
|
|
9
|
+
- Salesforce: CRM of record for lead capture and sales qualification
|
|
10
|
+
- HubSpot: marketing automation, nurture, lifecycle segmentation
|
|
11
|
+
|
|
12
|
+
## Trigger
|
|
13
|
+
|
|
14
|
+
- Scheduled incremental sync using Salesforce `LastModifiedDate`
|
|
15
|
+
- Optional near-real-time trigger from Salesforce CDC or platform events
|
|
16
|
+
|
|
17
|
+
## Source of truth
|
|
18
|
+
|
|
19
|
+
- Lead identity and sales-owned fields: Salesforce
|
|
20
|
+
- Marketing engagement and automation state: HubSpot
|
|
21
|
+
- Identity dedup anchor: email, with explicit mapping store as a secondary safeguard
|
|
22
|
+
|
|
23
|
+
## Sequence
|
|
24
|
+
|
|
25
|
+
1. Query Salesforce Leads modified since the last sync watermark, with a small overlap window.
|
|
26
|
+
2. Skip converted Leads and Leads with no email.
|
|
27
|
+
3. Search HubSpot Contacts by email.
|
|
28
|
+
4. If a Contact exists, update it with mapped fields.
|
|
29
|
+
5. If no Contact exists, create one and persist the mapping.
|
|
30
|
+
6. Record sync outcome per Lead so failed rows can be retried independently.
|
|
31
|
+
7. Advance the watermark only after all rows are processed or quarantined.
|
|
32
|
+
|
|
33
|
+
## Field mapping
|
|
34
|
+
|
|
35
|
+
| Salesforce Lead | HubSpot Contact |
|
|
36
|
+
|-----------------|-----------------|
|
|
37
|
+
| `Lead.Id` | custom property `salesforce_lead_id` or integration DB |
|
|
38
|
+
| `Email` | `email` |
|
|
39
|
+
| `FirstName` | `firstname` |
|
|
40
|
+
| `LastName` | `lastname` |
|
|
41
|
+
| `Company` | `company` |
|
|
42
|
+
| `Phone` | `phone` |
|
|
43
|
+
| `LeadSource` | custom source property |
|
|
44
|
+
| `Status` | derived `lifecyclestage` mapping |
|
|
45
|
+
|
|
46
|
+
## Idempotency
|
|
47
|
+
|
|
48
|
+
- Primary dedup key: normalized email
|
|
49
|
+
- Secondary correlation key: Salesforce Lead ID
|
|
50
|
+
- Do not use HubSpot batch create blindly for unknown contacts; search first
|
|
51
|
+
|
|
52
|
+
## Retry and partial failure policy
|
|
53
|
+
|
|
54
|
+
- Retry transient HubSpot rate-limit and availability errors per shard
|
|
55
|
+
- Quarantine `400` validation failures with Lead ID, email, and failing field names
|
|
56
|
+
- If one row fails, continue the rest of the batch and surface a remediation report
|
|
57
|
+
|
|
58
|
+
## Reconciliation
|
|
59
|
+
|
|
60
|
+
- Daily compare: Salesforce Leads updated in the last 24 hours vs HubSpot Contacts carrying `salesforce_lead_id`
|
|
61
|
+
- Repair records where the ID mapping is missing but email matches
|
|
62
|
+
- Flag email collisions where multiple Salesforce Leads compete for one HubSpot Contact
|
|
63
|
+
|
|
64
|
+
## Rollback guidance
|
|
65
|
+
|
|
66
|
+
- Avoid destructive deletes across systems
|
|
67
|
+
- If a bad sync overwrites HubSpot fields, restore from HubSpot history or your sync audit log and re-run the corrected mapping
|
|
68
|
+
|
|
69
|
+
## Observability
|
|
70
|
+
|
|
71
|
+
- Track processed, created, updated, quarantined, and retried rows per run
|
|
72
|
+
- Alert on spikes in duplicate resolution or validation failures
|
|
73
|
+
- Monitor watermark lag so the sync does not silently fall behind
|
|
74
|
+
|
|
75
|
+
## Related skills
|
|
76
|
+
|
|
77
|
+
- [`skills/salesforce/skill.md`](../skills/salesforce/skill.md)
|
|
78
|
+
- [`skills/hubspot/skill.md`](../skills/hubspot/skill.md)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Zendesk Ticket -> Jira Bug Escalation
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Escalate a Zendesk support ticket into Jira when support determines the issue is a product defect that engineering must track and resolve.
|
|
6
|
+
|
|
7
|
+
## Systems involved
|
|
8
|
+
|
|
9
|
+
- Zendesk: intake, customer communication, support triage
|
|
10
|
+
- Jira: engineering backlog, bug lifecycle, fix tracking
|
|
11
|
+
|
|
12
|
+
## Trigger
|
|
13
|
+
|
|
14
|
+
Any of:
|
|
15
|
+
- Zendesk ticket gets tag `engineering`
|
|
16
|
+
- Custom field `issue_type` changes to `bug`
|
|
17
|
+
- Ticket priority becomes `urgent` and product area is set
|
|
18
|
+
|
|
19
|
+
## Source of truth
|
|
20
|
+
|
|
21
|
+
- Customer communication and ticket state: Zendesk
|
|
22
|
+
- Engineering execution state: Jira
|
|
23
|
+
- Escalation linkage: integration database or mirrored external ID field
|
|
24
|
+
|
|
25
|
+
## Sequence
|
|
26
|
+
|
|
27
|
+
1. Receive the Zendesk webhook or consume the ticket via Incremental Export.
|
|
28
|
+
2. Build a deterministic correlation key: `zendesk:{ticket_id}`.
|
|
29
|
+
3. Search Jira for an existing issue with that correlation key in a custom field or label.
|
|
30
|
+
4. If no issue exists, create a Jira Bug with summary, reproduction notes, impact, and Zendesk URL.
|
|
31
|
+
5. If an issue already exists, update priority, labels, assignee, and current reproduction context.
|
|
32
|
+
6. Write the Jira issue key back to Zendesk as an internal note or custom field.
|
|
33
|
+
7. Optionally mirror selected Jira status changes back to Zendesk after state mapping is finalized.
|
|
34
|
+
|
|
35
|
+
## Field mapping
|
|
36
|
+
|
|
37
|
+
| Zendesk | Jira |
|
|
38
|
+
|---------|------|
|
|
39
|
+
| `ticket.id` | custom external ID / label |
|
|
40
|
+
| `subject` | `summary` |
|
|
41
|
+
| latest public comment | `description` |
|
|
42
|
+
| `priority` | `priority` |
|
|
43
|
+
| `tags` | `labels` |
|
|
44
|
+
| requester org / account | custom customer field or component |
|
|
45
|
+
| product area custom field | component / label |
|
|
46
|
+
|
|
47
|
+
## Idempotency
|
|
48
|
+
|
|
49
|
+
- Dedup key: `zendesk:{ticket_id}`
|
|
50
|
+
- Before create, always search Jira by dedup key
|
|
51
|
+
- Zendesk webhooks are at-least-once delivery, so the create path must be safe to replay
|
|
52
|
+
|
|
53
|
+
## Retry and partial failure policy
|
|
54
|
+
|
|
55
|
+
- Retry transient Jira failures (`429`, `503`) with bounded exponential backoff
|
|
56
|
+
- Do not retry `400` field validation errors blindly; quarantine and surface them
|
|
57
|
+
- If Jira create succeeds but Zendesk write-back fails, reconcile by searching Jira with the dedup key on the next run
|
|
58
|
+
|
|
59
|
+
## Reconciliation
|
|
60
|
+
|
|
61
|
+
- Daily job: compare Zendesk tickets tagged `engineering` against Jira bugs with the dedup key
|
|
62
|
+
- Repair missing backlinks in Zendesk
|
|
63
|
+
- Flag Jira issues missing an active Zendesk ticket counterpart
|
|
64
|
+
|
|
65
|
+
## Rollback guidance
|
|
66
|
+
|
|
67
|
+
- If a Jira issue was created in error, transition it to a closed/cancelled state instead of deleting it
|
|
68
|
+
- Remove the escalation tag or set a custom field in Zendesk to prevent automatic recreation
|
|
69
|
+
|
|
70
|
+
## Observability
|
|
71
|
+
|
|
72
|
+
- Count escalations per day
|
|
73
|
+
- Alert on create failures, write-back failures, or rising duplicate-detection rates
|
|
74
|
+
- Track mean time from Zendesk escalation to Jira issue creation
|
|
75
|
+
|
|
76
|
+
## Related skills
|
|
77
|
+
|
|
78
|
+
- [`skills/zendesk/skill.md`](../skills/zendesk/skill.md)
|
|
79
|
+
- [`skills/jira/skill.md`](../skills/jira/skill.md)
|
package/skills/INDEX.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
A curated library of integration skill documents for the most common SaaS platforms used in go-to-market and operations workflows. Each skill doc teaches an agent or automation builder how to reliably use that platform's API for high-value, production-grade workflows.
|
|
4
4
|
|
|
5
5
|
→ See [ROADMAP.md](./ROADMAP.md) for the prioritized build plan, phased milestones, and governance model.
|
|
6
|
+
→ Cross-tool workflow guides now live in [`../playbooks/INDEX.md`](../playbooks/INDEX.md).
|
|
6
7
|
|
|
7
8
|
---
|
|
8
9
|
|
package/skills/ROADMAP.md
CHANGED
|
@@ -12,14 +12,14 @@ This skill set enables end-to-end automation and integration across the most com
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
## Current state (as of 2026-03-
|
|
15
|
+
## Current state (as of 2026-03-12)
|
|
16
16
|
|
|
17
17
|
### What's shipped
|
|
18
18
|
|
|
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.
|
|
22
|
+
| **MCP server** — `clawskills-mcp` v0.4.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.
|
|
86
|
+
| npm (`clawskills-mcp`) | ✅ Live — v0.4.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 |
|
|
@@ -108,6 +108,43 @@ Each recipe lives as a `## Cross-tool recipe` appendix section in the relevant p
|
|
|
108
108
|
|
|
109
109
|
---
|
|
110
110
|
|
|
111
|
+
### Priority 5 — Workflow playbooks and retrieval UX
|
|
112
|
+
|
|
113
|
+
The next product layer is workflow-first guidance, not only tool-by-tool references. This means shipping opinionated playbooks that show how multiple systems fit together operationally, then exposing them cleanly through MCP.
|
|
114
|
+
|
|
115
|
+
**Backlog:**
|
|
116
|
+
- Add a `playbooks/` directory with high-value cross-tool workflows
|
|
117
|
+
- Ship 3 flagship playbooks first:
|
|
118
|
+
- Zendesk Ticket -> Jira Bug escalation
|
|
119
|
+
- HubSpot Deal Won -> Asana onboarding kickoff
|
|
120
|
+
- Salesforce Lead -> HubSpot Contact sync
|
|
121
|
+
- Expand to additional high-frequency workflows:
|
|
122
|
+
- Slack incident -> Jira issue
|
|
123
|
+
- GitHub PR -> Slack notification
|
|
124
|
+
- Zendesk -> Salesforce case sync
|
|
125
|
+
- Stripe payment failed -> HubSpot task
|
|
126
|
+
- Notion request -> Asana task
|
|
127
|
+
- Standardize each playbook format:
|
|
128
|
+
- business trigger
|
|
129
|
+
- systems involved
|
|
130
|
+
- source-of-truth decision
|
|
131
|
+
- field mapping
|
|
132
|
+
- idempotency strategy
|
|
133
|
+
- retry and partial-failure policy
|
|
134
|
+
- reconciliation and rollback guidance
|
|
135
|
+
- observability / alerting checks
|
|
136
|
+
- Expose playbooks through MCP after the doc layer stabilizes:
|
|
137
|
+
- `list_playbooks`
|
|
138
|
+
- `get_playbook`
|
|
139
|
+
- search/ranking that prefers recipes and playbooks over generic mentions
|
|
140
|
+
|
|
141
|
+
**Acceptance criteria:**
|
|
142
|
+
- [ ] At least 3 production-grade playbooks checked into the repo
|
|
143
|
+
- [ ] README and INDEX reference the playbooks layer
|
|
144
|
+
- [ ] MCP server plan defined for playbook retrieval and discovery
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
111
148
|
## Prioritization framework
|
|
112
149
|
|
|
113
150
|
Each skill/recipe is scored on five dimensions (1–5 each):
|
package/skills/asana/skill.md
CHANGED
|
@@ -685,6 +685,32 @@ curl -X POST \
|
|
|
685
685
|
|
|
686
686
|
---
|
|
687
687
|
|
|
688
|
+
### Cross-tool recipe: HubSpot Deal Won -> Asana project kickoff
|
|
689
|
+
|
|
690
|
+
**Goal:** Start onboarding or implementation work in Asana when a HubSpot Deal reaches `closedwon`.
|
|
691
|
+
|
|
692
|
+
**Flow:**
|
|
693
|
+
1. Receive a HubSpot webhook for `deal.propertyChange`.
|
|
694
|
+
2. Confirm the new `dealstage` maps to closed-won in the target pipeline.
|
|
695
|
+
3. Fetch the Deal, associated Company, and primary Contact from HubSpot.
|
|
696
|
+
4. Create the Asana task or project in the correct team or project template.
|
|
697
|
+
5. Store the Asana `task_gid` or `project_gid` back in HubSpot or in your integration DB.
|
|
698
|
+
|
|
699
|
+
**Suggested Asana fields:**
|
|
700
|
+
|
|
701
|
+
| Asana field | Source |
|
|
702
|
+
|---|---|
|
|
703
|
+
| `name` | `Onboard {dealname}` |
|
|
704
|
+
| `notes` | Deal URL, customer, ARR, owner, launch context |
|
|
705
|
+
| `assignee` | mapped CSM or implementation owner |
|
|
706
|
+
| `due_on` | kickoff or target launch date |
|
|
707
|
+
| `custom_fields` | plan, ARR, onboarding tier, deal ID |
|
|
708
|
+
|
|
709
|
+
**Operational guardrails:**
|
|
710
|
+
- Prevent duplicates by checking your mapping store before creating a new Asana record.
|
|
711
|
+
- If you use a project template, create the project once and then create the kickoff task inside it.
|
|
712
|
+
- HubSpot webhooks are at-least-once delivery; your creation path must be idempotent.
|
|
713
|
+
|
|
688
714
|
## Query patterns & filtering
|
|
689
715
|
|
|
690
716
|
### Task search
|
package/skills/hubspot/skill.md
CHANGED
|
@@ -390,6 +390,67 @@ GET https://api.hubapi.com/crm/v3/imports/{importId}
|
|
|
390
390
|
# importStatus: STARTED → PROCESSING → DONE
|
|
391
391
|
```
|
|
392
392
|
|
|
393
|
+
**When to use Imports vs batch endpoints:**
|
|
394
|
+
- Use `batch/create` or `batch/update` for API-driven writes where you already have structured JSON and need immediate responses.
|
|
395
|
+
- Use Imports for backfills, CSV-based migrations, or cases where users export flat files from another system.
|
|
396
|
+
|
|
397
|
+
**Partial failure handling:**
|
|
398
|
+
- Imports can finish with row-level failures even when the job completes successfully.
|
|
399
|
+
- Always inspect the import summary and error rows before marking the source batch as complete.
|
|
400
|
+
- Treat duplicate email collisions as data-quality events, not transient API failures.
|
|
401
|
+
|
|
402
|
+
**Batch write guardrails:**
|
|
403
|
+
- Max 100 inputs per batch request.
|
|
404
|
+
- For large syncs, shard into batches of 100 and apply a short concurrency limit so you do not burst into 429s.
|
|
405
|
+
- Search first when deduplicating by email; batch create is not idempotent.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
### Cross-tool recipe: Salesforce Lead -> HubSpot Contact sync
|
|
410
|
+
|
|
411
|
+
**Goal:** Mirror new or updated Salesforce Leads into HubSpot Contacts for marketing and lifecycle automation.
|
|
412
|
+
|
|
413
|
+
**HubSpot-side pattern:**
|
|
414
|
+
1. Search Contacts by `email`.
|
|
415
|
+
2. If found, `PATCH /crm/v3/objects/contacts/{id}` with mapped fields.
|
|
416
|
+
3. If not found, `POST /crm/v3/objects/contacts`.
|
|
417
|
+
4. Optionally stamp a custom property like `salesforce_lead_id` so future reconciliations do not depend solely on email.
|
|
418
|
+
|
|
419
|
+
**Recommended property mapping:**
|
|
420
|
+
|
|
421
|
+
| HubSpot Contact | Source from Salesforce |
|
|
422
|
+
|---|---|
|
|
423
|
+
| `email` | `Lead.Email` |
|
|
424
|
+
| `firstname` | `Lead.FirstName` |
|
|
425
|
+
| `lastname` | `Lead.LastName` |
|
|
426
|
+
| `company` | `Lead.Company` |
|
|
427
|
+
| `phone` | `Lead.Phone` |
|
|
428
|
+
| `lifecyclestage` | derived mapping from lead status |
|
|
429
|
+
|
|
430
|
+
**Failure policy:**
|
|
431
|
+
- `400` validation errors: quarantine the record with the source Lead ID and failing field names.
|
|
432
|
+
- `409` or duplicate conflicts: re-run a search by email, then update the winning Contact instead of creating a second record.
|
|
433
|
+
- `429`: respect `Retry-After` and retry the shard, not the full sync job.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
### Cross-tool recipe: HubSpot Deal Won -> Asana project kickoff
|
|
438
|
+
|
|
439
|
+
**Goal:** When a Deal enters `closedwon`, create onboarding work in Asana.
|
|
440
|
+
|
|
441
|
+
**Flow:**
|
|
442
|
+
1. Subscribe to `deal.propertyChange` for `dealstage`.
|
|
443
|
+
2. Filter for the pipeline stage representing "closed won".
|
|
444
|
+
3. Fetch the Deal plus associated Company and primary Contact.
|
|
445
|
+
4. Create the Asana task or project with the customer name, ARR, owner, and launch date.
|
|
446
|
+
5. Store the resulting Asana `gid` in HubSpot or in your integration DB.
|
|
447
|
+
|
|
448
|
+
**Minimum payload to send to Asana:**
|
|
449
|
+
- `name`: `Onboard {dealname}`
|
|
450
|
+
- `notes`: include the HubSpot deal URL, company, close date, and owner
|
|
451
|
+
- `due_on`: derived from kickoff SLA or launch date
|
|
452
|
+
- `custom_fields`: ARR, segment, onboarding tier, source deal ID
|
|
453
|
+
|
|
393
454
|
---
|
|
394
455
|
|
|
395
456
|
## Query patterns & filtering
|
package/skills/jira/skill.md
CHANGED
|
@@ -462,6 +462,36 @@ The `expand=transitions.fields` parameter returns the fields required for each t
|
|
|
462
462
|
|
|
463
463
|
---
|
|
464
464
|
|
|
465
|
+
### Cross-tool recipe: Zendesk Ticket -> Jira Bug escalation
|
|
466
|
+
|
|
467
|
+
**Goal:** Create or update a Jira Bug when a Zendesk ticket meets escalation criteria.
|
|
468
|
+
|
|
469
|
+
**Escalation trigger examples:**
|
|
470
|
+
- Zendesk ticket priority becomes `urgent`
|
|
471
|
+
- Tag includes `engineering`
|
|
472
|
+
- Custom field indicates product defect
|
|
473
|
+
|
|
474
|
+
**Flow:**
|
|
475
|
+
1. Receive the Zendesk webhook or poll Incremental Export.
|
|
476
|
+
2. Build a deterministic external key, for example `zendesk:{ticket_id}`.
|
|
477
|
+
3. Search Jira for an existing issue with that external key in a custom field or label.
|
|
478
|
+
4. If found, update severity, assignee, and status. If not found, create a Bug with the Zendesk URL in the description.
|
|
479
|
+
5. Post the Jira issue key back to Zendesk as an internal note or custom field.
|
|
480
|
+
|
|
481
|
+
**Minimum field mapping:**
|
|
482
|
+
|
|
483
|
+
| Zendesk | Jira |
|
|
484
|
+
|---|---|
|
|
485
|
+
| `subject` | `summary` |
|
|
486
|
+
| `description` / latest public comment | `description` (ADF) |
|
|
487
|
+
| `priority` | `priority` |
|
|
488
|
+
| `ticket id` | custom external ID field |
|
|
489
|
+
| `tags` | `labels` |
|
|
490
|
+
|
|
491
|
+
**Status sync recommendation:**
|
|
492
|
+
- One-way sync into Jira is safer initially.
|
|
493
|
+
- Add reverse Jira -> Zendesk status updates only after you define an explicit state map and conflict rules.
|
|
494
|
+
|
|
465
495
|
## Query patterns & filtering
|
|
466
496
|
|
|
467
497
|
### JQL incremental sync
|
|
@@ -378,10 +378,28 @@ curl -s "$BASE/services/data/v66.0/jobs/ingest/$JOB_ID" \
|
|
|
378
378
|
# Step 5 — retrieve failed records
|
|
379
379
|
curl -s "$BASE/services/data/v66.0/jobs/ingest/$JOB_ID/failedResults" \
|
|
380
380
|
-H "Authorization: Bearer $TOKEN"
|
|
381
|
+
|
|
382
|
+
# Step 6 — retrieve successful and unprocessed records for reconciliation
|
|
383
|
+
curl -s "$BASE/services/data/v66.0/jobs/ingest/$JOB_ID/successfulResults" \
|
|
384
|
+
-H "Authorization: Bearer $TOKEN"
|
|
385
|
+
|
|
386
|
+
curl -s "$BASE/services/data/v66.0/jobs/ingest/$JOB_ID/unprocessedRecords" \
|
|
387
|
+
-H "Authorization: Bearer $TOKEN"
|
|
381
388
|
```
|
|
382
389
|
|
|
383
390
|
**When to use Bulk API v2:** >1,000 records. For <1,000 records, REST is simpler and faster.
|
|
384
391
|
|
|
392
|
+
**CSV and job design tips:**
|
|
393
|
+
- Keep one object type per job. Do not mix Leads and Contacts in the same ingest file.
|
|
394
|
+
- Include only columns you intend to write. Extra CSV columns are treated as field references and can fail the row.
|
|
395
|
+
- Shard very large backfills into multiple jobs so a single failure does not block the full dataset.
|
|
396
|
+
- Reconcile with all three result streams: `successfulResults`, `failedResults`, and `unprocessedRecords`.
|
|
397
|
+
|
|
398
|
+
**Partial-failure policy:**
|
|
399
|
+
- Retry only `unprocessedRecords` automatically.
|
|
400
|
+
- Send `failedResults` to a remediation queue with the Salesforce error message and original source key.
|
|
401
|
+
- Do not advance the upstream source cursor until every row is either successful or quarantined.
|
|
402
|
+
|
|
385
403
|
---
|
|
386
404
|
|
|
387
405
|
### Recipe 8: Register a Change Data Capture (CDC) subscription
|
|
@@ -410,6 +428,33 @@ def handler(event):
|
|
|
410
428
|
|
|
411
429
|
---
|
|
412
430
|
|
|
431
|
+
### Cross-tool recipe: Salesforce Lead -> HubSpot Contact sync
|
|
432
|
+
|
|
433
|
+
**Goal:** Create or update a HubSpot Contact whenever a Salesforce Lead is created or updated, deduplicating by email.
|
|
434
|
+
|
|
435
|
+
**Flow:**
|
|
436
|
+
1. Query Salesforce Leads modified since the last sync boundary using `LastModifiedDate`.
|
|
437
|
+
2. Skip records with blank `Email` or `IsConverted = true`.
|
|
438
|
+
3. In HubSpot, search by `email` first. If found, `PATCH` the Contact. If not, create it.
|
|
439
|
+
4. Store the mapping `Lead.Id -> HubSpot contact id` in your integration DB, but treat `email` as the primary dedup key.
|
|
440
|
+
5. Write a sync marker back to Salesforce only after the HubSpot write succeeds.
|
|
441
|
+
|
|
442
|
+
**Field mapping (minimum viable):**
|
|
443
|
+
|
|
444
|
+
| Salesforce Lead | HubSpot Contact |
|
|
445
|
+
|---|---|
|
|
446
|
+
| `Email` | `email` |
|
|
447
|
+
| `FirstName` | `firstname` |
|
|
448
|
+
| `LastName` | `lastname` |
|
|
449
|
+
| `Company` | `company` |
|
|
450
|
+
| `Phone` | `phone` |
|
|
451
|
+
| `LeadSource` | `lead_source` or custom property |
|
|
452
|
+
|
|
453
|
+
**Operational guardrails:**
|
|
454
|
+
- Prefer a watermark sync (`LastModifiedDate >= cursor`) plus a small overlap window to avoid missed updates.
|
|
455
|
+
- Do not use Bulk API v2 for near-real-time sync unless you are processing a large backfill.
|
|
456
|
+
- If HubSpot rejects one record, quarantine that record and continue the rest of the sync batch.
|
|
457
|
+
|
|
413
458
|
## Query patterns & filtering
|
|
414
459
|
|
|
415
460
|
### SOQL reference patterns
|
package/skills/zendesk/skill.md
CHANGED
|
@@ -493,6 +493,11 @@ curl -u "agent@example.com/token:TOKEN" \
|
|
|
493
493
|
|
|
494
494
|
**Validation**: Poll job status to `completed`. Spot-check 5 random ticket IDs from the batch to confirm `assignee_id` updated correctly.
|
|
495
495
|
|
|
496
|
+
**Operational notes:**
|
|
497
|
+
- Use `update_many` for homogeneous changes like tags, assignee, or status across a known set of ticket IDs.
|
|
498
|
+
- Keep request batches at 100 IDs max and persist the returned `job_status_id`.
|
|
499
|
+
- If the job partially succeeds, re-run only the failed IDs after inspecting the job result.
|
|
500
|
+
|
|
496
501
|
---
|
|
497
502
|
|
|
498
503
|
### Recipe 8: Manage agent permissions and group membership
|
|
@@ -542,6 +547,39 @@ curl -u "admin@example.com/token:TOKEN" \
|
|
|
542
547
|
|
|
543
548
|
---
|
|
544
549
|
|
|
550
|
+
### Cross-tool recipe: Zendesk Ticket -> Jira Bug escalation
|
|
551
|
+
|
|
552
|
+
**Goal**: Escalate a Zendesk ticket into Jira when support confirms the issue is a product defect.
|
|
553
|
+
|
|
554
|
+
**Trigger examples**:
|
|
555
|
+
- Agent adds tag `engineering`
|
|
556
|
+
- Ticket priority changes to `urgent`
|
|
557
|
+
- Custom field `issue_type` becomes `bug`
|
|
558
|
+
|
|
559
|
+
**Flow**:
|
|
560
|
+
1. Capture the Zendesk event via webhook or Incremental Export.
|
|
561
|
+
2. Build a stable external key such as `zendesk:{ticket_id}`.
|
|
562
|
+
3. Search Jira for an existing issue using that key in a custom field or label.
|
|
563
|
+
4. If no issue exists, create a Jira Bug with the Zendesk URL, customer impact, and reproduction details.
|
|
564
|
+
5. Write the Jira issue key back to Zendesk as an internal note or custom field so agents can track engineering progress.
|
|
565
|
+
|
|
566
|
+
**Minimum field mapping**:
|
|
567
|
+
|
|
568
|
+
| Zendesk ticket field | Jira field |
|
|
569
|
+
|---|---|
|
|
570
|
+
| `subject` | `summary` |
|
|
571
|
+
| latest public comment | `description` |
|
|
572
|
+
| `priority` | `priority` |
|
|
573
|
+
| `tags` | `labels` |
|
|
574
|
+
| `id` | external ID field / label |
|
|
575
|
+
|
|
576
|
+
**Operational guardrails**:
|
|
577
|
+
- Make the create path idempotent; webhooks can be retried.
|
|
578
|
+
- Keep status sync one-way at first. Bi-directional sync needs an explicit state map and conflict resolution policy.
|
|
579
|
+
- Post only sanitized customer context into Jira if Zendesk comments may include PII.
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
545
583
|
## Query patterns & filtering
|
|
546
584
|
|
|
547
585
|
### Search API
|
|
@@ -602,6 +640,13 @@ GET /api/v2/incremental/tickets.json?start_time=1708300800
|
|
|
602
640
|
- Also available for users and organizations: `/api/v2/incremental/users.json`, `/api/v2/incremental/organizations.json`.
|
|
603
641
|
- Events include `deleted` tickets — check `status` field.
|
|
604
642
|
|
|
643
|
+
**Production sync pattern:**
|
|
644
|
+
1. Start with `start_time` at least 5 minutes in the past.
|
|
645
|
+
2. Continue following the returned window until you have drained the stream for the current run.
|
|
646
|
+
3. Persist the returned `end_time`, not your request timestamp.
|
|
647
|
+
4. Re-run with a small overlap window to protect against delayed indexing and consumer crashes.
|
|
648
|
+
5. Deduplicate by ticket `id` and latest `updated_at` in your sink.
|
|
649
|
+
|
|
605
650
|
### Sorting
|
|
606
651
|
|
|
607
652
|
Search API: `&sort_by=created_at&sort_order=asc` (or `desc`).
|
|
@@ -648,6 +693,13 @@ def zendesk_request(method, url, **kwargs):
|
|
|
648
693
|
raise Exception("Exceeded retry budget")
|
|
649
694
|
```
|
|
650
695
|
|
|
696
|
+
### Bulk update and export guardrails
|
|
697
|
+
|
|
698
|
+
- `update_many` is efficient for operational changes across up to 100 tickets, but it is not a substitute for a full sync.
|
|
699
|
+
- Poll the returned job status before assuming the bulk update succeeded.
|
|
700
|
+
- For analytics or warehouse sync, use Incremental Export rather than paginating Search or list endpoints.
|
|
701
|
+
- Keep writes and reads separate: use `update_many` for operational changes, Incremental Export for system-of-record ingestion.
|
|
702
|
+
|
|
651
703
|
### Retry vs fail-fast
|
|
652
704
|
|
|
653
705
|
| HTTP Status | Action |
|