@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/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
+ }
@@ -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
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
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';
@@ -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
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
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 server_exports = {};
20
- __export(server_exports, {
21
- server: () => server
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
- async function apiGet(path) {
36
- const res = await fetch(`${apiBase()}${path}`, {
37
- headers: { Accept: "application/json", "User-Agent": "gera-skills-mcp/0.1.0" }
38
- });
39
- if (!res.ok) {
40
- throw new Error(`GeraSkills API ${path} returned ${res.status}`);
41
- }
42
- return res.json();
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
- async function apiPost(path, body, authToken) {
45
- const res = await fetch(`${apiBase()}${path}`, {
46
- method: "POST",
47
- headers: {
48
- Accept: "application/json",
49
- "Content-Type": "application/json",
50
- "User-Agent": "gera-skills-mcp/0.1.0",
51
- ...authToken ? { Authorization: `Bearer ${authToken}` } : {}
52
- },
53
- body: JSON.stringify(body)
54
- });
55
- if (!res.ok) {
56
- const errText = await res.text();
57
- throw new Error(`GeraSkills API ${path} returned ${res.status}: ${errText.slice(0, 200)}`);
58
- }
59
- return res.json();
60
- }
61
- const server = new import_mcp.McpServer({
62
- name: "gera-skills",
63
- version: "0.1.0"
64
- });
65
- server.tool(
66
- "search_skills",
67
- "Search the GeraSkills catalogue for robot capability packs. Returns paginated results with id, slug, name, description, price (GBP pence), rating, download count, creator, and category. Use filters to narrow by category, compatible robot, price band, or minimum rating.",
68
- {
69
- query: import_zod.z.string().max(200).optional().describe("Full-text query"),
70
- category: import_zod.z.string().max(120).optional().describe("Category slug"),
71
- compatibleRobot: import_zod.z.string().max(200).optional().describe("Robot model identifier \u2014 e.g. boston-dynamics-spot"),
72
- minPriceGbpPence: import_zod.z.number().int().min(0).optional(),
73
- maxPriceGbpPence: import_zod.z.number().int().min(0).optional(),
74
- minRating: import_zod.z.number().min(0).max(5).optional(),
75
- sort: import_zod.z.enum(["new", "top", "price_asc", "price_desc", "rating"]).optional(),
76
- page: import_zod.z.number().int().min(1).default(1),
77
- pageSize: import_zod.z.number().int().min(1).max(50).default(20)
78
- },
79
- async (args) => {
80
- const qs = new URLSearchParams();
81
- for (const [k, v] of Object.entries(args)) {
82
- if (v !== void 0 && v !== null && v !== "") {
83
- qs.set(k, String(v));
84
- }
85
- }
86
- const result = await apiGet(`/api/v1/skills?${qs.toString()}`);
87
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
88
- }
89
- );
90
- server.tool(
91
- "get_skill",
92
- "Fetch full metadata for a single GeraSkill by slug or id. Returns description, long description, version history, compatibility floor, creator info, price, and aggregate rating.",
93
- {
94
- slug: import_zod.z.string().min(1).max(200).describe("Skill slug (kebab-case) or UUID")
95
- },
96
- async ({ slug }) => {
97
- const result = await apiGet(`/api/v1/skills/${encodeURIComponent(slug)}`);
98
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
99
- }
100
- );
101
- server.tool(
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: "operator_consent_required",
142
- message: "install_skill requires an operator consent token. Set GERA_USER_TOKEN to the operator JWT before invoking."
143
- },
144
- null,
145
- 2
146
- )
147
- }
148
- ],
149
- isError: true
150
- };
151
- }
152
- const result = await apiPost(
153
- "/api/v1/purchases",
154
- { skillId, robotId, ...version ? { version } : {} },
155
- token
156
- );
157
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
158
- }
159
- );
160
- async function main() {
161
- const transport = new import_stdio.StdioServerTransport();
162
- await server.connect(transport);
163
- console.error("GeraSkills MCP server v0.1.0 running on stdio");
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
- main().catch((err) => {
166
- console.error(err);
167
- process.exit(1);
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
+ }