@gera-services/mcp-gera-jobs 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.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * GeraJobs MCP Server (stdio)
3
+ *
4
+ * Exposes the Gera global jobs dataset to AI agents over the Model Context
5
+ * Protocol. Everything is answered locally from the same on-disk data the
6
+ * GeraJobs product renders — 30 job categories, 20 country job markets, and
7
+ * salary benchmarks aggregated from 1,002 real crawled listings — so an agent
8
+ * (Claude, ChatGPT with tools, any MCP client) can search categories, read
9
+ * salary benchmarks, and look up country markets entirely offline, no auth.
10
+ *
11
+ * Product: GeraJobs — https://gerajobs.com (a Gera Systems product)
12
+ */
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
+ export declare const server: McpServer;
15
+ export declare function main(): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,248 @@
1
+ /**
2
+ * GeraJobs MCP Server (stdio)
3
+ *
4
+ * Exposes the Gera global jobs dataset to AI agents over the Model Context
5
+ * Protocol. Everything is answered locally from the same on-disk data the
6
+ * GeraJobs product renders — 30 job categories, 20 country job markets, and
7
+ * salary benchmarks aggregated from 1,002 real crawled listings — so an agent
8
+ * (Claude, ChatGPT with tools, any MCP client) can search categories, read
9
+ * salary benchmarks, and look up country markets entirely offline, no auth.
10
+ *
11
+ * Product: GeraJobs — https://gerajobs.com (a Gera Systems product)
12
+ */
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { z } from 'zod';
16
+ import { JOB_CATEGORIES, JOB_MARKETS, SALARY_ROLES, SALARY_TOTAL_LISTINGS, SALARY_GENERATED_AT, } from './dataset.generated.js';
17
+ export const server = new McpServer({
18
+ name: 'gera-jobs',
19
+ version: '1.0.0',
20
+ });
21
+ function asText(payload) {
22
+ return {
23
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
24
+ };
25
+ }
26
+ const SOURCE_NOTE = 'Data from GeraJobs (gerajobs.com), a Gera Systems product. Salary figures are aggregated ' +
27
+ `from ${SALARY_TOTAL_LISTINGS} real crawled job listings (snapshot ${SALARY_GENERATED_AT}); ` +
28
+ 'category and market figures are GeraJobs editorial benchmarks. Orientation only.';
29
+ function matchesQuery(cat, q) {
30
+ const hay = (cat.name +
31
+ ' ' +
32
+ cat.description +
33
+ ' ' +
34
+ cat.topSkills.join(' ') +
35
+ ' ' +
36
+ cat.keywords.join(' ')).toLowerCase();
37
+ return hay.includes(q);
38
+ }
39
+ function categorySummary(cat) {
40
+ return {
41
+ id: cat.id,
42
+ name: cat.name,
43
+ description: cat.description,
44
+ avg_salary_usd: cat.avgSalaryUSD,
45
+ top_skills: cat.topSkills,
46
+ trending: cat.trending,
47
+ remote_percentage: cat.remotePercentage,
48
+ };
49
+ }
50
+ // ── Tool 1: search_job_categories ────────────────────────────────────────────
51
+ // Free-text search over the 30 real GeraJobs categories (name, description,
52
+ // skills, keywords). The agent's main entry point from a plain-language query.
53
+ server.registerTool('search_job_categories', {
54
+ title: 'Search GeraJobs job categories',
55
+ description: 'Search the 30 GeraJobs job categories by free-text query (matches name, description, required skills and SEO keywords). Use this first when a user describes a kind of work in plain language (e.g. "machine learning", "nursing", "remote design", "plumber"). Returns matching categories with average USD salary range, top skills, remote percentage and whether the field is trending. Pass no query to list every category.',
56
+ inputSchema: {
57
+ query: z
58
+ .string()
59
+ .optional()
60
+ .describe('Free-text query, e.g. "machine learning" or "remote sales". Omit to list all.'),
61
+ trending_only: z
62
+ .boolean()
63
+ .optional()
64
+ .describe('If true, only return categories flagged as trending in 2026.'),
65
+ limit: z
66
+ .number()
67
+ .int()
68
+ .min(1)
69
+ .max(30)
70
+ .optional()
71
+ .describe('Max categories to return (default 10).'),
72
+ },
73
+ }, async ({ query, trending_only, limit }) => {
74
+ const q = (query ?? '').trim().toLowerCase();
75
+ let matches = q ? JOB_CATEGORIES.filter((c) => matchesQuery(c, q)) : [...JOB_CATEGORIES];
76
+ if (trending_only)
77
+ matches = matches.filter((c) => c.trending);
78
+ const max = limit ?? 10;
79
+ return asText({
80
+ query: query ?? null,
81
+ trending_only: trending_only ?? false,
82
+ total_matches: matches.length,
83
+ categories: matches.slice(0, max).map(categorySummary),
84
+ source: SOURCE_NOTE,
85
+ });
86
+ });
87
+ // ── Tool 2: get_salary_benchmark ─────────────────────────────────────────────
88
+ // Real salary benchmarks per role per market, from 1,002 crawled listings.
89
+ const SALARY_ROLE_SLUGS = SALARY_ROLES.map((r) => r.slug);
90
+ server.registerTool('get_salary_benchmark', {
91
+ title: 'Get a GeraJobs salary benchmark',
92
+ description: 'Return real salary benchmarks for a role, aggregated from crawled GeraJobs listings. For each market (country/currency) you get the sample size and median / 25th / 75th percentile / low / high annual pay — every number traces to real listings. Available roles: ' +
93
+ SALARY_ROLE_SLUGS.join(', ') +
94
+ '. Optionally filter to one country by ISO code (e.g. "US", "GB"). Omit currency to get all markets for the role.',
95
+ inputSchema: {
96
+ role: z
97
+ .string()
98
+ .describe('Role slug or name, e.g. "software-engineer", "Data Scientist", "product-manager".'),
99
+ country_code: z
100
+ .string()
101
+ .length(2)
102
+ .optional()
103
+ .describe('Optional ISO-3166 country code to filter to one market, e.g. "US", "GB", "CA".'),
104
+ },
105
+ }, async ({ role, country_code }) => {
106
+ const needle = role.trim().toLowerCase().replace(/\s+/g, '-');
107
+ const found = SALARY_ROLES.find((r) => r.slug === needle) ??
108
+ SALARY_ROLES.find((r) => r.slug.includes(needle) || r.name.toLowerCase().includes(role.trim().toLowerCase()));
109
+ if (!found) {
110
+ return asText({
111
+ error: `No salary benchmark for role "${role}".`,
112
+ available_roles: SALARY_ROLES.map((r) => ({ slug: r.slug, name: r.name })),
113
+ source: SOURCE_NOTE,
114
+ });
115
+ }
116
+ let markets = found.markets;
117
+ if (country_code) {
118
+ const cc = country_code.toUpperCase();
119
+ markets = markets.filter((m) => m.countryCode === cc);
120
+ }
121
+ return asText({
122
+ role: { slug: found.slug, name: found.name },
123
+ total_sample_size: found.totalSampleSize,
124
+ markets,
125
+ sample_listings: found.sampleListings.slice(0, 5),
126
+ methodology: 'Annual figures only; hourly rows excluded. Median/percentiles computed per role × currency from crawled listings.',
127
+ source: SOURCE_NOTE,
128
+ });
129
+ });
130
+ // ── Tool 3: list_job_categories ──────────────────────────────────────────────
131
+ // Compact index of every category + slug, so an agent can map descriptions to ids.
132
+ server.registerTool('list_job_categories', {
133
+ title: 'List all GeraJobs categories',
134
+ description: 'List all 30 GeraJobs job categories with their id, name, average USD salary range and trending flag. Use this to get the canonical category ids to pass to other tools, or to give a user a full menu of fields GeraJobs covers.',
135
+ inputSchema: {},
136
+ }, async () => asText({
137
+ count: JOB_CATEGORIES.length,
138
+ categories: JOB_CATEGORIES.map((c) => ({
139
+ id: c.id,
140
+ name: c.name,
141
+ avg_salary_usd: c.avgSalaryUSD,
142
+ trending: c.trending,
143
+ remote_percentage: c.remotePercentage,
144
+ })),
145
+ source: SOURCE_NOTE,
146
+ }));
147
+ // ── Tool 4: get_job_market ───────────────────────────────────────────────────
148
+ // Country-level hiring intelligence for the 20 real GeraJobs markets.
149
+ server.registerTool('get_job_market', {
150
+ title: 'Get a GeraJobs country job market',
151
+ description: 'Return the hiring profile for a country GeraJobs covers: average monthly wage + currency, top industries, top employers, job/AI growth rates, remote-work adoption and a market summary. Look up by ISO country code (e.g. "AM", "GB", "US"), country name, or city. Omit all arguments to list every covered market.',
152
+ inputSchema: {
153
+ country_code: z.string().optional().describe('ISO-3166 country code, e.g. "AM", "GE", "GB".'),
154
+ query: z
155
+ .string()
156
+ .optional()
157
+ .describe('Country name or city, e.g. "Armenia", "Yerevan", "United Kingdom".'),
158
+ },
159
+ }, async ({ country_code, query }) => {
160
+ if (!country_code && !query) {
161
+ return asText({
162
+ count: JOB_MARKETS.length,
163
+ markets: JOB_MARKETS.map((m) => ({
164
+ country_code: m.countryCode,
165
+ country: m.country,
166
+ city: m.city,
167
+ currency: m.currency,
168
+ })),
169
+ source: SOURCE_NOTE,
170
+ });
171
+ }
172
+ let match = null;
173
+ if (country_code) {
174
+ const cc = country_code.toUpperCase();
175
+ match = JOB_MARKETS.find((m) => m.countryCode === cc) ?? null;
176
+ }
177
+ if (!match && query) {
178
+ const q = query.trim().toLowerCase();
179
+ match =
180
+ JOB_MARKETS.find((m) => m.country.toLowerCase() === q || m.city.toLowerCase() === q || m.slug === q) ??
181
+ JOB_MARKETS.find((m) => m.country.toLowerCase().includes(q) || m.city.toLowerCase().includes(q)) ??
182
+ null;
183
+ }
184
+ if (!match) {
185
+ return asText({
186
+ error: `No GeraJobs market for "${country_code ?? query}".`,
187
+ available_markets: JOB_MARKETS.map((m) => ({
188
+ country_code: m.countryCode,
189
+ country: m.country,
190
+ })),
191
+ source: SOURCE_NOTE,
192
+ });
193
+ }
194
+ return asText({ market: match, source: SOURCE_NOTE });
195
+ });
196
+ // ── Tool 5: get_category_details ─────────────────────────────────────────────
197
+ // Full detail + FAQ for one category — the GEO/answer-engine payload.
198
+ server.registerTool('get_category_details', {
199
+ title: 'Get full GeraJobs category details',
200
+ description: 'Return the full GeraJobs profile for one job category: description, average USD salary range, the complete top-skills list, remote percentage, and the curated FAQ (common questions and GeraJobs answers about salaries, skills, and how to find roles). Look up by category id (from list_job_categories) or name.',
201
+ inputSchema: {
202
+ category: z
203
+ .string()
204
+ .describe('Category id or name, e.g. "software-engineering" or "Data Science & AI".'),
205
+ },
206
+ }, async ({ category }) => {
207
+ const q = category.trim().toLowerCase();
208
+ const found = JOB_CATEGORIES.find((c) => c.id === q) ??
209
+ JOB_CATEGORIES.find((c) => c.name.toLowerCase() === q) ??
210
+ JOB_CATEGORIES.find((c) => c.id.includes(q) || c.name.toLowerCase().includes(q));
211
+ if (!found) {
212
+ return asText({
213
+ error: `No GeraJobs category for "${category}".`,
214
+ available_categories: JOB_CATEGORIES.map((c) => ({ id: c.id, name: c.name })),
215
+ source: SOURCE_NOTE,
216
+ });
217
+ }
218
+ return asText({
219
+ id: found.id,
220
+ name: found.name,
221
+ description: found.description,
222
+ avg_salary_usd: found.avgSalaryUSD,
223
+ top_skills: found.topSkills,
224
+ trending: found.trending,
225
+ remote_percentage: found.remotePercentage,
226
+ faqs: found.faqs,
227
+ source: SOURCE_NOTE,
228
+ });
229
+ });
230
+ // ── Start ────────────────────────────────────────────────────────────────────
231
+ export async function main() {
232
+ const transport = new StdioServerTransport();
233
+ await server.connect(transport);
234
+ // stderr only — stdout is the MCP transport.
235
+ console.error('GeraJobs MCP server running on stdio (gera-jobs v1.0.0)');
236
+ }
237
+ // Run when invoked directly as the built server (node dist/server.js). The
238
+ // bin/cli.js entry imports and calls main() itself, so we only auto-run for
239
+ // direct server.js invocation to avoid a double start.
240
+ const isMain = typeof process !== 'undefined' &&
241
+ process.argv[1] &&
242
+ /server\.js$/.test(process.argv[1]);
243
+ if (isMain) {
244
+ main().catch((err) => {
245
+ console.error('Fatal:', err);
246
+ process.exit(1);
247
+ });
248
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@gera-services/mcp-gera-jobs",
3
+ "version": "1.0.0",
4
+ "description": "GeraJobs MCP server — search global job categories, read real salary benchmarks, and look up country job markets. Offline, no auth. A Gera Systems product.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/server.js",
8
+ "types": "dist/server.d.ts",
9
+ "mcpName": "io.github.geraservicesuk/mcp-gera-jobs",
10
+ "bin": {
11
+ "mcp-gera-jobs": "bin/cli.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "bin",
16
+ "server.json",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build:dataset": "node scripts/build-dataset.mjs",
21
+ "build": "tsc --noCheck",
22
+ "type-check": "tsc --noEmit",
23
+ "start": "node bin/cli.js",
24
+ "smoke": "node scripts/smoke.mjs"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "model-context-protocol",
29
+ "jobs",
30
+ "job-search",
31
+ "salary-benchmark",
32
+ "gera",
33
+ "gera-jobs",
34
+ "recruitment"
35
+ ],
36
+ "author": "Gera Systems Ltd",
37
+ "homepage": "https://gerajobs.com",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/geraservicesuk/globetura.git",
41
+ "directory": "packages/mcp-gera-jobs"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.12.0",
45
+ "zod": "^3.23.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20.12.0",
49
+ "typescript": "^5.4.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=20"
53
+ }
54
+ }
package/server.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.geraservicesuk/mcp-gera-jobs",
4
+ "description": "Search global job categories, real crawled salary benchmarks and country job markets. No auth.",
5
+ "version": "1.0.0",
6
+ "repository": {
7
+ "url": "https://github.com/geraservicesuk/globetura",
8
+ "source": "github",
9
+ "subfolder": "packages/mcp-gera-jobs"
10
+ },
11
+ "websiteUrl": "https://gerajobs.com",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "identifier": "@gera-services/mcp-gera-jobs",
16
+ "version": "1.0.0",
17
+ "transport": {
18
+ "type": "stdio"
19
+ }
20
+ }
21
+ ]
22
+ }