@gera-services/mcp-gera-skills 0.1.0 → 1.0.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/LICENSE +1 -1
- package/README.md +90 -19
- package/bin/cli.js +11 -1
- package/dist/data/skill-packs.json +2687 -0
- package/dist/data.d.ts +39 -0
- package/dist/data.js +80 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -28
- package/dist/server.d.ts +22 -0
- package/dist/server.js +223 -167
- package/package.json +23 -26
- package/server.json +8 -19
package/dist/data.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface SkillPack {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
category: string;
|
|
5
|
+
subcategory?: string;
|
|
6
|
+
description: string;
|
|
7
|
+
capabilities: string[];
|
|
8
|
+
priceMonthly: number;
|
|
9
|
+
priceOneOff?: number;
|
|
10
|
+
tags: string[];
|
|
11
|
+
targetUser: string;
|
|
12
|
+
region?: string[];
|
|
13
|
+
badge?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const SKILLS: SkillPack[];
|
|
16
|
+
export declare const CATEGORIES: string[];
|
|
17
|
+
export declare const CATALOGUE_SOURCE: string;
|
|
18
|
+
/**
|
|
19
|
+
* Public marketplace URL for a skill. Matches the canonical route generated
|
|
20
|
+
* by the live product at `/skills/[slug]` (slug === pack id), per
|
|
21
|
+
* `apps/gera-skills-web/src/app/skills/[slug]/page.tsx` generateStaticParams.
|
|
22
|
+
*/
|
|
23
|
+
export declare function skillUrl(id: string): string;
|
|
24
|
+
export interface SearchFilters {
|
|
25
|
+
query?: string;
|
|
26
|
+
category?: string;
|
|
27
|
+
region?: string;
|
|
28
|
+
maxPriceMonthly?: number;
|
|
29
|
+
badge?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Filter the catalogue. All filters are AND-combined; omitted filters match all. */
|
|
32
|
+
export declare function searchSkills(f: SearchFilters): SkillPack[];
|
|
33
|
+
/** Find one skill by exact id, or by case-insensitive name match. */
|
|
34
|
+
export declare function getSkill(idOrName: string): SkillPack | undefined;
|
|
35
|
+
/** Category names with their skill counts, sorted by count desc. */
|
|
36
|
+
export declare function categoryCounts(): {
|
|
37
|
+
category: string;
|
|
38
|
+
count: number;
|
|
39
|
+
}[];
|
package/dist/data.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline GeraSkills catalogue.
|
|
3
|
+
*
|
|
4
|
+
* The data is a verbatim snapshot of the real GeraSkills web catalogue
|
|
5
|
+
* (`apps/gera-skills-web/src/data/skill-packs.ts`, 104 skill packs across 12
|
|
6
|
+
* categories), generated into `data/skill-packs.json` and loaded at startup.
|
|
7
|
+
* Because it is embedded, every tool answers entirely offline — no backend,
|
|
8
|
+
* no network, no auth — so any MCP client can browse the same catalogue the
|
|
9
|
+
* GeraSkills product ships.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
// data file sits next to the compiled module: dist/data/skill-packs.json
|
|
16
|
+
const dataPath = join(here, 'data', 'skill-packs.json');
|
|
17
|
+
const dataset = JSON.parse(readFileSync(dataPath, 'utf8'));
|
|
18
|
+
export const SKILLS = dataset.skills;
|
|
19
|
+
export const CATEGORIES = dataset.categories;
|
|
20
|
+
export const CATALOGUE_SOURCE = dataset.source;
|
|
21
|
+
/**
|
|
22
|
+
* Public marketplace URL for a skill. Matches the canonical route generated
|
|
23
|
+
* by the live product at `/skills/[slug]` (slug === pack id), per
|
|
24
|
+
* `apps/gera-skills-web/src/app/skills/[slug]/page.tsx` generateStaticParams.
|
|
25
|
+
*/
|
|
26
|
+
export function skillUrl(id) {
|
|
27
|
+
return `https://skills.gera.services/skills/${id}`;
|
|
28
|
+
}
|
|
29
|
+
/** Lower-cased haystack of every searchable field on a skill. */
|
|
30
|
+
function haystack(s) {
|
|
31
|
+
return [
|
|
32
|
+
s.name,
|
|
33
|
+
s.category,
|
|
34
|
+
s.subcategory ?? '',
|
|
35
|
+
s.description,
|
|
36
|
+
s.capabilities.join(' '),
|
|
37
|
+
s.tags.join(' '),
|
|
38
|
+
s.targetUser,
|
|
39
|
+
(s.region ?? []).join(' '),
|
|
40
|
+
]
|
|
41
|
+
.join(' ')
|
|
42
|
+
.toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
/** Filter the catalogue. All filters are AND-combined; omitted filters match all. */
|
|
45
|
+
export function searchSkills(f) {
|
|
46
|
+
const q = f.query?.trim().toLowerCase();
|
|
47
|
+
const cat = f.category?.trim().toLowerCase();
|
|
48
|
+
const region = f.region?.trim().toLowerCase();
|
|
49
|
+
const badge = f.badge?.trim().toLowerCase();
|
|
50
|
+
return SKILLS.filter((s) => {
|
|
51
|
+
if (q && !haystack(s).includes(q))
|
|
52
|
+
return false;
|
|
53
|
+
if (cat && s.category.toLowerCase() !== cat)
|
|
54
|
+
return false;
|
|
55
|
+
if (region && !(s.region ?? []).some((r) => r.toLowerCase() === region))
|
|
56
|
+
return false;
|
|
57
|
+
if (f.maxPriceMonthly !== undefined && s.priceMonthly > f.maxPriceMonthly)
|
|
58
|
+
return false;
|
|
59
|
+
if (badge && (s.badge ?? '').toLowerCase() !== badge)
|
|
60
|
+
return false;
|
|
61
|
+
return true;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Find one skill by exact id, or by case-insensitive name match. */
|
|
65
|
+
export function getSkill(idOrName) {
|
|
66
|
+
const key = idOrName.trim().toLowerCase();
|
|
67
|
+
return (SKILLS.find((s) => s.id.toLowerCase() === key) ??
|
|
68
|
+
SKILLS.find((s) => s.name.toLowerCase() === key));
|
|
69
|
+
}
|
|
70
|
+
/** Category names with their skill counts, sorted by count desc. */
|
|
71
|
+
export function categoryCounts() {
|
|
72
|
+
const counts = new Map();
|
|
73
|
+
for (const c of CATEGORIES)
|
|
74
|
+
counts.set(c, 0);
|
|
75
|
+
for (const s of SKILLS)
|
|
76
|
+
counts.set(s.category, (counts.get(s.category) ?? 0) + 1);
|
|
77
|
+
return [...counts.entries()]
|
|
78
|
+
.map(([category, count]) => ({ category, count }))
|
|
79
|
+
.sort((a, b) => b.count - a.count);
|
|
80
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gera-services/mcp-gera-skills
|
|
3
|
+
*
|
|
4
|
+
* MCP server for GeraSkills — the marketplace of AI skill packs from Gera
|
|
5
|
+
* Systems. Re-exports the server, the tool registrar, and the offline
|
|
6
|
+
* catalogue helpers for programmatic use (e.g. mounting on a Streamable HTTP
|
|
7
|
+
* transport).
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
export { server, registerTools, main } from './server.js';
|
|
12
|
+
export { SKILLS, CATEGORIES, searchSkills, getSkill, categoryCounts, skillUrl, type SkillPack, } from './data.js';
|
package/dist/index.js
CHANGED
|
@@ -1,28 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
var index_exports = {};
|
|
20
|
-
__export(index_exports, {
|
|
21
|
-
server: () => import_server.server
|
|
22
|
-
});
|
|
23
|
-
module.exports = __toCommonJS(index_exports);
|
|
24
|
-
var import_server = require("./server.js");
|
|
25
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
26
|
-
0 && (module.exports = {
|
|
27
|
-
server
|
|
28
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @gera-services/mcp-gera-skills
|
|
3
|
+
*
|
|
4
|
+
* MCP server for GeraSkills — the marketplace of AI skill packs from Gera
|
|
5
|
+
* Systems. Re-exports the server, the tool registrar, and the offline
|
|
6
|
+
* catalogue helpers for programmatic use (e.g. mounting on a Streamable HTTP
|
|
7
|
+
* transport).
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
export { server, registerTools, main } from './server.js';
|
|
12
|
+
export { SKILLS, CATEGORIES, searchSkills, getSkill, categoryCounts, skillUrl, } from './data.js';
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeraSkills MCP Server (stdio)
|
|
3
|
+
*
|
|
4
|
+
* Exposes the GeraSkills marketplace catalogue to AI agents over the Model
|
|
5
|
+
* Context Protocol. The full catalogue (104 skill packs across 12 categories)
|
|
6
|
+
* is embedded from the real GeraSkills web data, so every tool answers
|
|
7
|
+
* entirely offline — no backend, no network, no auth required. An agent
|
|
8
|
+
* (Claude, ChatGPT with tools, any MCP client) can search skills, read a
|
|
9
|
+
* skill's full detail, recommend skills for a goal, browse categories, and
|
|
10
|
+
* find skills relevant to a country/region.
|
|
11
|
+
*
|
|
12
|
+
* Product: GeraSkills — https://skills.gera.services (a Gera Systems product)
|
|
13
|
+
*/
|
|
14
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
|
+
/**
|
|
16
|
+
* Register all GeraSkills tools on a given McpServer instance.
|
|
17
|
+
* Transport-agnostic: the same handlers serve stdio (local) and Streamable
|
|
18
|
+
* HTTP / SSE (hosted) clients unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export declare function registerTools(server: McpServer): void;
|
|
21
|
+
export declare const server: McpServer;
|
|
22
|
+
export declare function main(): Promise<void>;
|
package/dist/server.js
CHANGED
|
@@ -1,172 +1,228 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
23
|
-
module.exports = __toCommonJS(server_exports);
|
|
24
|
-
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
25
|
-
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
26
|
-
var import_zod = require("zod");
|
|
27
|
-
const DEFAULT_API = "https://geraskills.com/backend";
|
|
28
|
-
function apiBase() {
|
|
29
|
-
return process.env.GERASKILLS_API_URL ?? DEFAULT_API;
|
|
30
|
-
}
|
|
31
|
-
function userToken() {
|
|
32
|
-
const t = process.env.GERA_USER_TOKEN;
|
|
33
|
-
return t && t.trim() ? t.trim() : void 0;
|
|
1
|
+
/**
|
|
2
|
+
* GeraSkills MCP Server (stdio)
|
|
3
|
+
*
|
|
4
|
+
* Exposes the GeraSkills marketplace catalogue to AI agents over the Model
|
|
5
|
+
* Context Protocol. The full catalogue (104 skill packs across 12 categories)
|
|
6
|
+
* is embedded from the real GeraSkills web data, so every tool answers
|
|
7
|
+
* entirely offline — no backend, no network, no auth required. An agent
|
|
8
|
+
* (Claude, ChatGPT with tools, any MCP client) can search skills, read a
|
|
9
|
+
* skill's full detail, recommend skills for a goal, browse categories, and
|
|
10
|
+
* find skills relevant to a country/region.
|
|
11
|
+
*
|
|
12
|
+
* Product: GeraSkills — https://skills.gera.services (a Gera Systems product)
|
|
13
|
+
*/
|
|
14
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { SKILLS, CATEGORIES, CATALOGUE_SOURCE, searchSkills, getSkill, categoryCounts, skillUrl, } from './data.js';
|
|
18
|
+
function asText(payload) {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
21
|
+
};
|
|
34
22
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
23
|
+
/** Compact card used in search/list results (full detail via get_skill). */
|
|
24
|
+
function card(s) {
|
|
25
|
+
return {
|
|
26
|
+
id: s.id,
|
|
27
|
+
name: s.name,
|
|
28
|
+
category: s.category,
|
|
29
|
+
subcategory: s.subcategory ?? null,
|
|
30
|
+
description: s.description,
|
|
31
|
+
price_monthly_usd: s.priceMonthly,
|
|
32
|
+
price_one_off_usd: s.priceOneOff ?? null,
|
|
33
|
+
badge: s.badge ?? null,
|
|
34
|
+
tags: s.tags,
|
|
35
|
+
url: skillUrl(s.id),
|
|
36
|
+
};
|
|
43
37
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
"list_categories",
|
|
103
|
-
"Return the GeraSkills category tree as a flat list (id, slug, name, parent_id). Agents should use this to narrow search scope before calling search_skills.",
|
|
104
|
-
{},
|
|
105
|
-
async () => {
|
|
106
|
-
const result = await apiGet("/api/v1/categories");
|
|
107
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
108
|
-
}
|
|
109
|
-
);
|
|
110
|
-
server.tool(
|
|
111
|
-
"get_creator_revenue",
|
|
112
|
-
"Return lifetime revenue and install count for the creator of one or more skills. Useful for agents evaluating trustworthiness before recommending a skill. Requires an operator JWT (set GERA_USER_TOKEN) if the creator is not the calling user; otherwise returns a limited public view.",
|
|
113
|
-
{
|
|
114
|
-
creatorId: import_zod.z.string().min(1).max(200).describe("Creator ID (UUID)")
|
|
115
|
-
},
|
|
116
|
-
async () => {
|
|
117
|
-
const token = userToken();
|
|
118
|
-
const endpoint = token ? "/api/v1/creators/me/payouts" : "/api/v1/skills?sort=top&pageSize=1";
|
|
119
|
-
const result = await apiGet(endpoint);
|
|
120
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
server.tool(
|
|
124
|
-
"install_skill",
|
|
125
|
-
"Initiate purchase + install of a GeraSkill on a specific robot. Creates a Stripe PaymentIntent (70% creator / 30% platform) and returns the client_secret plus amount. REQUIRES an operator consent token \u2014 set GERA_USER_TOKEN. The calling agent MUST surface the displayed price to the user and receive explicit approval before calling this tool.",
|
|
126
|
-
{
|
|
127
|
-
skillId: import_zod.z.string().min(1).max(200),
|
|
128
|
-
robotId: import_zod.z.string().min(1).max(200),
|
|
129
|
-
version: import_zod.z.string().regex(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/).max(50).optional().describe("Explicit semver version; defaults to skill.current_version_id")
|
|
130
|
-
},
|
|
131
|
-
async ({ skillId, robotId, version }) => {
|
|
132
|
-
const token = userToken();
|
|
133
|
-
if (!token) {
|
|
134
|
-
return {
|
|
135
|
-
content: [
|
|
136
|
-
{
|
|
137
|
-
type: "text",
|
|
138
|
-
text: JSON.stringify(
|
|
139
|
-
{
|
|
38
|
+
const categoryEnum = z
|
|
39
|
+
.string()
|
|
40
|
+
.describe(`Exact category name. One of: ${CATEGORIES.join(', ')}.`);
|
|
41
|
+
/**
|
|
42
|
+
* Register all GeraSkills tools on a given McpServer instance.
|
|
43
|
+
* Transport-agnostic: the same handlers serve stdio (local) and Streamable
|
|
44
|
+
* HTTP / SSE (hosted) clients unchanged.
|
|
45
|
+
*/
|
|
46
|
+
export function registerTools(server) {
|
|
47
|
+
// ── Tool 1: search_skills ──────────────────────────────────────────────────
|
|
48
|
+
server.registerTool('search_skills', {
|
|
49
|
+
title: 'Search the GeraSkills catalogue',
|
|
50
|
+
description: 'Full-text search the GeraSkills marketplace of AI skill packs. Match against name, description, capabilities, tags, and target user, with optional filters by category, region, maximum monthly price (USD), and badge. Returns compact cards (id, name, category, description, price, tags, marketplace url). Offline — searches the embedded 104-skill catalogue.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
query: z.string().optional().describe('Free-text query, e.g. "GDPR", "soil", "essay".'),
|
|
53
|
+
category: categoryEnum.optional(),
|
|
54
|
+
region: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('Region/country tag a skill is relevant to, e.g. "UK", "EU", "US", "Global".'),
|
|
58
|
+
max_price_monthly_usd: z
|
|
59
|
+
.number()
|
|
60
|
+
.min(0)
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Only return skills at or below this monthly price in USD.'),
|
|
63
|
+
badge: z
|
|
64
|
+
.enum(['New', 'Popular', 'Pro'])
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('Filter to a marketplace badge.'),
|
|
67
|
+
limit: z.number().int().min(1).max(104).default(20).describe('Max results to return.'),
|
|
68
|
+
},
|
|
69
|
+
}, async ({ query, category, region, max_price_monthly_usd, badge, limit }) => {
|
|
70
|
+
const matches = searchSkills({
|
|
71
|
+
query,
|
|
72
|
+
category,
|
|
73
|
+
region,
|
|
74
|
+
maxPriceMonthly: max_price_monthly_usd,
|
|
75
|
+
badge,
|
|
76
|
+
});
|
|
77
|
+
const cap = limit ?? 20;
|
|
78
|
+
return asText({
|
|
79
|
+
query: { query: query ?? null, category: category ?? null, region: region ?? null, max_price_monthly_usd: max_price_monthly_usd ?? null, badge: badge ?? null },
|
|
80
|
+
total_matches: matches.length,
|
|
81
|
+
returned: Math.min(matches.length, cap),
|
|
82
|
+
results: matches.slice(0, cap).map(card),
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
// ── Tool 2: get_skill ──────────────────────────────────────────────────────
|
|
86
|
+
server.registerTool('get_skill', {
|
|
87
|
+
title: 'Get full detail for one GeraSkill',
|
|
88
|
+
description: 'Return the complete record for a single skill pack by its id (e.g. "gdpr-compliance-checker") or exact name. Includes every capability, both price options (monthly + one-off USD), tags, target user, applicable regions, badge, and the marketplace URL.',
|
|
89
|
+
inputSchema: {
|
|
90
|
+
id: z.string().describe('Skill id (kebab-case) or exact skill name.'),
|
|
91
|
+
},
|
|
92
|
+
}, async ({ id }) => {
|
|
93
|
+
const s = getSkill(id);
|
|
94
|
+
if (!s) {
|
|
95
|
+
return asText({
|
|
140
96
|
ok: false,
|
|
141
|
-
error:
|
|
142
|
-
message:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
97
|
+
error: 'not_found',
|
|
98
|
+
message: `No skill matches "${id}". Use search_skills or list_categories to find a valid id.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return asText({
|
|
102
|
+
id: s.id,
|
|
103
|
+
name: s.name,
|
|
104
|
+
category: s.category,
|
|
105
|
+
subcategory: s.subcategory ?? null,
|
|
106
|
+
description: s.description,
|
|
107
|
+
capabilities: s.capabilities,
|
|
108
|
+
price_monthly_usd: s.priceMonthly,
|
|
109
|
+
price_one_off_usd: s.priceOneOff ?? null,
|
|
110
|
+
tags: s.tags,
|
|
111
|
+
target_user: s.targetUser,
|
|
112
|
+
regions: s.region ?? ['Global'],
|
|
113
|
+
badge: s.badge ?? null,
|
|
114
|
+
url: skillUrl(s.id),
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
// ── Tool 3: recommend_skills ───────────────────────────────────────────────
|
|
118
|
+
// A ranked "skill path" for a stated goal — relevance-scored across the catalogue.
|
|
119
|
+
server.registerTool('recommend_skills', {
|
|
120
|
+
title: 'Recommend a GeraSkills skill path for a goal',
|
|
121
|
+
description: 'Given a plain-language goal (e.g. "help me run a small UK accountancy practice" or "I want to grow vegetables and sell at market"), return a ranked, deduplicated list of the most relevant GeraSkills, scored by how many goal terms match each skill. Use this to assemble a "skill path" — the set of capability packs that together address the goal. Offline relevance ranking over the embedded catalogue.',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
goal: z.string().min(3).describe('What the user wants to accomplish, in their own words.'),
|
|
124
|
+
region: z
|
|
125
|
+
.string()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe('Bias toward skills tagged for this region, e.g. "UK".'),
|
|
128
|
+
limit: z.number().int().min(1).max(20).default(5),
|
|
129
|
+
},
|
|
130
|
+
}, async ({ goal, region, limit }) => {
|
|
131
|
+
const stop = new Set([
|
|
132
|
+
'the', 'a', 'an', 'to', 'for', 'and', 'or', 'of', 'in', 'on', 'my', 'me', 'i',
|
|
133
|
+
'want', 'need', 'help', 'with', 'how', 'do', 'can', 'is', 'are', 'be', 'run',
|
|
134
|
+
'small', 'business', 'get', 'make', 'using', 'use', 'practice', 'practise',
|
|
135
|
+
'manage', 'managing', 'start', 'starting', 'new', 'good', 'best',
|
|
136
|
+
]);
|
|
137
|
+
const terms = goal
|
|
138
|
+
.toLowerCase()
|
|
139
|
+
.split(/[^a-z0-9]+/)
|
|
140
|
+
.filter((t) => t.length > 2 && !stop.has(t));
|
|
141
|
+
// Match on a short stem so e.g. "accountancy" hits "accounting"/"accountant",
|
|
142
|
+
// "gardening" hits "garden", "negotiations" hits "negotiation".
|
|
143
|
+
const stems = terms.map((t) => (t.length > 5 ? t.slice(0, 5) : t));
|
|
144
|
+
const scored = SKILLS.map((s) => {
|
|
145
|
+
const hay = [
|
|
146
|
+
s.name, s.category, s.subcategory ?? '', s.description,
|
|
147
|
+
s.capabilities.join(' '), s.tags.join(' '), s.targetUser,
|
|
148
|
+
]
|
|
149
|
+
.join(' ')
|
|
150
|
+
.toLowerCase();
|
|
151
|
+
let score = 0;
|
|
152
|
+
for (const stem of stems)
|
|
153
|
+
if (hay.includes(stem))
|
|
154
|
+
score += 1;
|
|
155
|
+
// small region boost
|
|
156
|
+
if (region && (s.region ?? []).some((r) => r.toLowerCase() === region.trim().toLowerCase())) {
|
|
157
|
+
score += 0.5;
|
|
158
|
+
}
|
|
159
|
+
return { s, score };
|
|
160
|
+
})
|
|
161
|
+
.filter((x) => x.score > 0)
|
|
162
|
+
.sort((a, b) => b.score - a.score)
|
|
163
|
+
.slice(0, limit ?? 5);
|
|
164
|
+
return asText({
|
|
165
|
+
goal,
|
|
166
|
+
matched_terms: terms,
|
|
167
|
+
match_stems: stems,
|
|
168
|
+
count: scored.length,
|
|
169
|
+
skill_path: scored.map((x) => ({ relevance: x.score, ...card(x.s) })),
|
|
170
|
+
note: scored.length === 0
|
|
171
|
+
? 'No catalogue matches. Try list_categories then search_skills with a broader query.'
|
|
172
|
+
: 'Ranked by goal-term overlap; review each skill with get_skill before recommending.',
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
// ── Tool 4: list_categories ────────────────────────────────────────────────
|
|
176
|
+
server.registerTool('list_categories', {
|
|
177
|
+
title: 'List GeraSkills categories with counts',
|
|
178
|
+
description: 'Return all GeraSkills top-level categories with the number of skill packs in each, sorted by count. Use this to scope a subsequent search_skills call by category.',
|
|
179
|
+
inputSchema: {},
|
|
180
|
+
}, async () => asText({
|
|
181
|
+
catalogue_size: SKILLS.length,
|
|
182
|
+
category_count: CATEGORIES.length,
|
|
183
|
+
categories: categoryCounts(),
|
|
184
|
+
}));
|
|
185
|
+
// ── Tool 5: skills_for_region ──────────────────────────────────────────────
|
|
186
|
+
server.registerTool('skills_for_region', {
|
|
187
|
+
title: 'List GeraSkills relevant to a region/country',
|
|
188
|
+
description: 'Return the skills explicitly tagged as relevant to a given region or country (e.g. "UK", "EU", "US", "Global"). Useful for agents serving users in a specific jurisdiction where some skills are jurisdiction-specific (employment law, tax, energy tariffs, certification standards).',
|
|
189
|
+
inputSchema: {
|
|
190
|
+
region: z.string().describe('Region/country tag, e.g. "UK", "EU", "US", "Global".'),
|
|
191
|
+
limit: z.number().int().min(1).max(104).default(50),
|
|
192
|
+
},
|
|
193
|
+
}, async ({ region, limit }) => {
|
|
194
|
+
const matches = searchSkills({ region });
|
|
195
|
+
const cap = limit ?? 50;
|
|
196
|
+
return asText({
|
|
197
|
+
region,
|
|
198
|
+
total_matches: matches.length,
|
|
199
|
+
returned: Math.min(matches.length, cap),
|
|
200
|
+
results: matches.slice(0, cap).map(card),
|
|
201
|
+
});
|
|
202
|
+
});
|
|
164
203
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
170
|
-
0 && (module.exports = {
|
|
171
|
-
server
|
|
204
|
+
// ── Server instance ──────────────────────────────────────────────────────────
|
|
205
|
+
export const server = new McpServer({
|
|
206
|
+
name: 'gera-skills',
|
|
207
|
+
version: '1.0.0',
|
|
172
208
|
});
|
|
209
|
+
registerTools(server);
|
|
210
|
+
// ── Start ────────────────────────────────────────────────────────────────────
|
|
211
|
+
export async function main() {
|
|
212
|
+
const transport = new StdioServerTransport();
|
|
213
|
+
await server.connect(transport);
|
|
214
|
+
// stderr only — stdout is the MCP transport.
|
|
215
|
+
console.error(`GeraSkills MCP server running on stdio (gera-skills v1.0.0, ${SKILLS.length} skills, source ${CATALOGUE_SOURCE})`);
|
|
216
|
+
}
|
|
217
|
+
// Run when invoked directly as the built server (node dist/server.js).
|
|
218
|
+
// bin/cli.js imports and calls main() itself, so only auto-run for direct
|
|
219
|
+
// server.js invocation to avoid a double start.
|
|
220
|
+
const isMain = typeof process !== 'undefined' &&
|
|
221
|
+
process.argv[1] &&
|
|
222
|
+
/server\.js$/.test(process.argv[1]);
|
|
223
|
+
if (isMain) {
|
|
224
|
+
main().catch((err) => {
|
|
225
|
+
console.error('Fatal:', err);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
});
|
|
228
|
+
}
|