Skip to content

Commit 1040b76

Browse files
committed
feat: add dynamic ai-helpers skill fetching from GitHub
Add useAiHelpersSkill MCP tool and resource templates that fetch skills from ai-helpers at runtime. Disk cache fallback when GitHub is unreachable. Ref: PF-4034
1 parent 10a49da commit 1040b76

10 files changed

Lines changed: 461 additions & 52 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ dist/
99
build/
1010
*.tsbuildinfo
1111

12+
# Runtime cache
13+
.cache/
14+
1215
# Environment variables
1316
.env
1417
.env.local

cspell.config.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"language": "en",
33
"words": [
4+
"aihelpers",
45
"amet",
56
"codemods",
67
"ized",
@@ -12,6 +13,7 @@
1213
"onsessioninitialized",
1314
"onsessionclosed",
1415
"patternfly",
16+
"prerendered",
1517
"rereview",
1618
"rsort",
1719
"sparkline",

src/__tests__/__snapshots__/server.test.ts.snap

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,21 @@ exports[`runServer should allow server to be stopped, http stop server: diagnost
4242
[
4343
"Registered resource: patternfly-schemas-template",
4444
],
45+
[
46+
"Registered resource: aihelpers-skills-index",
47+
],
48+
[
49+
"Registered resource: aihelpers-skills-template",
50+
],
4551
[
4652
"Registered tool: usePatternFlyDocs",
4753
],
4854
[
4955
"Registered tool: searchPatternFlyDocs",
5056
],
57+
[
58+
"Registered tool: useAiHelpersSkill",
59+
],
5160
[
5261
"test-server server running on HTTP transport",
5362
],
@@ -104,12 +113,21 @@ exports[`runServer should allow server to be stopped, stdio stop server: diagnos
104113
[
105114
"Registered resource: patternfly-schemas-template",
106115
],
116+
[
117+
"Registered resource: aihelpers-skills-index",
118+
],
119+
[
120+
"Registered resource: aihelpers-skills-template",
121+
],
107122
[
108123
"Registered tool: usePatternFlyDocs",
109124
],
110125
[
111126
"Registered tool: searchPatternFlyDocs",
112127
],
128+
[
129+
"Registered tool: useAiHelpersSkill",
130+
],
113131
[
114132
"test-server server running on stdio transport",
115133
],
@@ -166,6 +184,12 @@ exports[`runServer should attempt to run server, create transport, connect, and
166184
[
167185
"Registered resource: patternfly-schemas-template",
168186
],
187+
[
188+
"Registered resource: aihelpers-skills-index",
189+
],
190+
[
191+
"Registered resource: aihelpers-skills-template",
192+
],
169193
[
170194
"test-server-4 server running on stdio transport",
171195
],
@@ -239,6 +263,12 @@ exports[`runServer should attempt to run server, disable SIGINT handler: diagnos
239263
[
240264
"Registered resource: patternfly-schemas-template",
241265
],
266+
[
267+
"Registered resource: aihelpers-skills-index",
268+
],
269+
[
270+
"Registered resource: aihelpers-skills-template",
271+
],
242272
[
243273
"test-server-7 server running on stdio transport",
244274
],
@@ -307,6 +337,12 @@ exports[`runServer should attempt to run server, enable SIGINT handler explicitl
307337
[
308338
"Registered resource: patternfly-schemas-template",
309339
],
340+
[
341+
"Registered resource: aihelpers-skills-index",
342+
],
343+
[
344+
"Registered resource: aihelpers-skills-template",
345+
],
310346
[
311347
"test-server-8 server running on stdio transport",
312348
],
@@ -380,6 +416,12 @@ exports[`runServer should attempt to run server, register a tool: diagnostics 1`
380416
[
381417
"Registered resource: patternfly-schemas-template",
382418
],
419+
[
420+
"Registered resource: aihelpers-skills-index",
421+
],
422+
[
423+
"Registered resource: aihelpers-skills-template",
424+
],
383425
[
384426
"Registered tool: loremIpsum",
385427
],
@@ -461,6 +503,12 @@ exports[`runServer should attempt to run server, register multiple tools: diagno
461503
[
462504
"Registered resource: patternfly-schemas-template",
463505
],
506+
[
507+
"Registered resource: aihelpers-skills-index",
508+
],
509+
[
510+
"Registered resource: aihelpers-skills-template",
511+
],
464512
[
465513
"Registered tool: loremIpsum",
466514
],
@@ -549,6 +597,12 @@ exports[`runServer should attempt to run server, use custom options: diagnostics
549597
[
550598
"Registered resource: patternfly-schemas-template",
551599
],
600+
[
601+
"Registered resource: aihelpers-skills-index",
602+
],
603+
[
604+
"Registered resource: aihelpers-skills-template",
605+
],
552606
[
553607
"test-server-3 server running on stdio transport",
554608
],
@@ -622,12 +676,21 @@ exports[`runServer should attempt to run server, use default tools, http: diagno
622676
[
623677
"Registered resource: patternfly-schemas-template",
624678
],
679+
[
680+
"Registered resource: aihelpers-skills-index",
681+
],
682+
[
683+
"Registered resource: aihelpers-skills-template",
684+
],
625685
[
626686
"Registered tool: usePatternFlyDocs",
627687
],
628688
[
629689
"Registered tool: searchPatternFlyDocs",
630690
],
691+
[
692+
"Registered tool: useAiHelpersSkill",
693+
],
631694
[
632695
"test-server-2 server running on HTTP transport",
633696
],
@@ -658,6 +721,7 @@ exports[`runServer should attempt to run server, use default tools, http: diagno
658721
"registerTool": [
659722
"usePatternFlyDocs",
660723
"searchPatternFlyDocs",
724+
"useAiHelpersSkill",
661725
],
662726
}
663727
`;
@@ -704,12 +768,21 @@ exports[`runServer should attempt to run server, use default tools, stdio: diagn
704768
[
705769
"Registered resource: patternfly-schemas-template",
706770
],
771+
[
772+
"Registered resource: aihelpers-skills-index",
773+
],
774+
[
775+
"Registered resource: aihelpers-skills-template",
776+
],
707777
[
708778
"Registered tool: usePatternFlyDocs",
709779
],
710780
[
711781
"Registered tool: searchPatternFlyDocs",
712782
],
783+
[
784+
"Registered tool: useAiHelpersSkill",
785+
],
713786
[
714787
"test-server-1 server running on stdio transport",
715788
],
@@ -740,6 +813,7 @@ exports[`runServer should attempt to run server, use default tools, stdio: diagn
740813
"registerTool": [
741814
"usePatternFlyDocs",
742815
"searchPatternFlyDocs",
816+
"useAiHelpersSkill",
743817
],
744818
}
745819
`;

src/aiHelpers.skills.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { resolve, dirname } from 'node:path';
2+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
3+
import { fileURLToPath } from 'node:url';
4+
import { memo } from './server.caching';
5+
import { log } from './logger';
6+
7+
interface AiHelpersSkill {
8+
name: string;
9+
plugin: string;
10+
description: string;
11+
content: string;
12+
}
13+
14+
interface AiHelpersSkillsData {
15+
version: string;
16+
generated: string;
17+
meta: {
18+
totalSkills: number;
19+
source: string;
20+
};
21+
skills: AiHelpersSkill[];
22+
}
23+
24+
const SKILLS_URL = 'https://raw.githubusercontent.com/patternfly/ai-helpers/main/dist/skills.json';
25+
26+
const FETCH_TIMEOUT_MS = 10_000;
27+
28+
const CACHE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '.cache');
29+
30+
const CACHE_FILE = resolve(CACHE_DIR, 'aiHelpers.skills.json');
31+
32+
const isValidSkillsData = (data: unknown): data is AiHelpersSkillsData =>
33+
Boolean(data) && typeof data === 'object' && Array.isArray((data as AiHelpersSkillsData).skills);
34+
35+
const readCachedSkills = async (): Promise<AiHelpersSkillsData | undefined> => {
36+
try {
37+
const raw = await readFile(CACHE_FILE, 'utf-8');
38+
const data = JSON.parse(raw);
39+
40+
if (isValidSkillsData(data)) {
41+
return data;
42+
}
43+
} catch {
44+
// No cache file or invalid — expected on first run
45+
}
46+
47+
return undefined;
48+
};
49+
50+
const writeCachedSkills = async (data: AiHelpersSkillsData): Promise<void> => {
51+
try {
52+
await mkdir(CACHE_DIR, { recursive: true });
53+
await writeFile(CACHE_FILE, JSON.stringify(data), 'utf-8');
54+
} catch (error) {
55+
log.warn(`Failed to write skills cache: ${error instanceof Error ? error.message : error}`);
56+
}
57+
};
58+
59+
const fetchSkillsData = async (): Promise<AiHelpersSkillsData> => {
60+
try {
61+
const controller = new AbortController();
62+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
63+
64+
const response = await fetch(SKILLS_URL, { signal: controller.signal });
65+
66+
clearTimeout(timeout);
67+
68+
if (!response.ok) {
69+
throw new Error(`HTTP ${response.status}`);
70+
}
71+
72+
const data = await response.json() as AiHelpersSkillsData;
73+
74+
if (!isValidSkillsData(data)) {
75+
throw new Error('Invalid skills data shape');
76+
}
77+
78+
log.info(`Loaded ${data.skills.length} ai-helpers skills from GitHub (generated ${data.generated})`);
79+
80+
await writeCachedSkills(data);
81+
82+
return data;
83+
} catch (error) {
84+
log.warn(`Failed to fetch ai-helpers skills from GitHub: ${error instanceof Error ? error.message : error}`);
85+
86+
const cached = await readCachedSkills();
87+
88+
if (cached) {
89+
log.info(`Using cached skills data (generated ${cached.generated})`);
90+
91+
return cached;
92+
}
93+
94+
log.warn('No cached skills available — skills will be empty until GitHub is reachable');
95+
96+
return { version: '0', generated: '', meta: { totalSkills: 0, source: 'none' }, skills: [] };
97+
}
98+
};
99+
100+
const fetchSkillsDataMemo = memo(fetchSkillsData, {
101+
cacheLimit: 1,
102+
expire: 5 * 60 * 1000,
103+
cacheErrors: false
104+
});
105+
106+
/**
107+
* Returns all ai-helpers skills, fetched from GitHub with disk cache fallback.
108+
*/
109+
const getAiHelpersSkills = async (): Promise<AiHelpersSkill[]> => {
110+
const data = await fetchSkillsDataMemo();
111+
112+
return data.skills;
113+
};
114+
115+
/**
116+
* Returns the full SKILL.md content for a given skill name.
117+
*
118+
* @param name - The skill name to look up.
119+
*/
120+
const getAiHelpersSkillContent = async (name: string): Promise<string | undefined> => {
121+
const data = await fetchSkillsDataMemo();
122+
const skill = data.skills.find(
123+
(entry: AiHelpersSkill) => entry.name.toLowerCase() === name.toLowerCase()
124+
);
125+
126+
return skill?.content;
127+
};
128+
129+
export { getAiHelpersSkills, getAiHelpersSkillContent, type AiHelpersSkill };
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { type McpResource } from './server';
2+
import { stringJoin } from './server.helpers';
3+
import { getAiHelpersSkills } from './aiHelpers.skills';
4+
5+
/**
6+
* Name of the resource.
7+
*/
8+
const NAME = 'aihelpers-skills-index';
9+
10+
/**
11+
* URI for the resource.
12+
*/
13+
const URI = 'aihelpers://skills/index';
14+
15+
/**
16+
* Resource configuration.
17+
*/
18+
const CONFIG = {
19+
title: 'ai-helpers Skills Index',
20+
description: 'Lists all available ai-helpers skills with names and descriptions.',
21+
mimeType: 'text/markdown' as const
22+
};
23+
24+
/**
25+
* Resource creator for the ai-helpers skills index.
26+
*/
27+
const aiHelpersSkillsIndexResource = (): McpResource => [
28+
NAME,
29+
URI,
30+
CONFIG,
31+
async (passedUri: URL) => {
32+
const skills = await getAiHelpersSkills();
33+
34+
const header = stringJoin.newline(
35+
'# ai-helpers Skills',
36+
'',
37+
'PatternFly coding skills served from the [ai-helpers](https://github.com/patternfly/ai-helpers) marketplace. Read any skill via `aihelpers://skills/{name}`.',
38+
''
39+
);
40+
41+
const table = stringJoin.newline(
42+
'| Skill | Plugin | Description |',
43+
'|-------|--------|-------------|',
44+
...skills.map(skill => `| ${skill.name} | ${skill.plugin} | ${skill.description} |`)
45+
);
46+
47+
return {
48+
contents: [
49+
{
50+
uri: passedUri?.toString(),
51+
mimeType: 'text/markdown',
52+
text: stringJoin.newline(header, table)
53+
}
54+
]
55+
};
56+
}
57+
];
58+
59+
export { aiHelpersSkillsIndexResource, NAME, URI, CONFIG };

0 commit comments

Comments
 (0)