usajobs-mcp 1.0.0 → 1.0.2
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 +19 -1
- package/dist/api.d.ts +20 -0
- package/dist/api.js +138 -0
- package/dist/license.d.ts +8 -0
- package/dist/license.js +78 -0
- package/dist/server.js +39 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,8 +18,26 @@ The server uses stdio, so it can be connected to Claude Desktop, Cursor, VS Code
|
|
|
18
18
|
|
|
19
19
|
## Tools at a glance
|
|
20
20
|
|
|
21
|
-
- `search_jobs`:
|
|
21
|
+
- `search_jobs`: US federal jobs by keyword and location, with pay bands and closing dates.
|
|
22
22
|
|
|
23
23
|
## Limits and privacy
|
|
24
24
|
|
|
25
25
|
This project is intentionally narrow. It should be treated as a practical helper, not a complete certification or security audit. Check the implementation and the returned data before using it with sensitive material. Your key stays local and is only sent to USAJOBS. Read-only.
|
|
26
|
+
|
|
27
|
+
<!-- paywall -->
|
|
28
|
+
## Premium tools
|
|
29
|
+
|
|
30
|
+
These tools need a license key:
|
|
31
|
+
|
|
32
|
+
* `hiring_rules`
|
|
33
|
+
|
|
34
|
+
Buy a key at https://mcp-marketplace.io/server/io-github-mrfentmen-usajobs-mcp and set it in your MCP client config:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
"env": { "MCP_LICENSE_KEY": "mcp_live_..." }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The key is checked against MCP Marketplace, cached for 24 hours, and keeps
|
|
41
|
+
working offline once one check has succeeded. Every other tool on this server
|
|
42
|
+
stays free.
|
|
43
|
+
<!-- /paywall -->
|
package/dist/api.d.ts
CHANGED
|
@@ -15,3 +15,23 @@ export declare function searchJobs(keyword: string, location?: string, limit?: n
|
|
|
15
15
|
jobs: FedJob[];
|
|
16
16
|
}>;
|
|
17
17
|
export declare function formatJob(j: FedJob, index?: number): string;
|
|
18
|
+
/**
|
|
19
|
+
* The federal employment rules themselves, as published: which document, which
|
|
20
|
+
* agency, what type of rule, when it took effect and what it says. These are the
|
|
21
|
+
* documents that decide who is eligible for a federal post, so a job search that
|
|
22
|
+
* finds an opening says nothing about whether the reader qualifies for it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function hiringRules(args: {
|
|
25
|
+
query?: string;
|
|
26
|
+
limit?: number;
|
|
27
|
+
}): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* The shape of federal employment activity over time rather than any one document:
|
|
30
|
+
* how many hiring-related rules were published each year, which agency filed them,
|
|
31
|
+
* what kind of rule they were, and which have actually taken effect. A trend is
|
|
32
|
+
* what tells you a policy is moving, and a single search result cannot.
|
|
33
|
+
*/
|
|
34
|
+
export declare function hiringSignal(args: {
|
|
35
|
+
query?: string;
|
|
36
|
+
years?: number;
|
|
37
|
+
}): Promise<string>;
|
package/dist/api.js
CHANGED
|
@@ -56,3 +56,141 @@ export function formatJob(j, index) {
|
|
|
56
56
|
const meta = [j.agency, j.locations.join("/"), j.pay, j.grade ? `GS-${j.grade}` : ""].filter(Boolean).join(" · ");
|
|
57
57
|
return `${prefix}${j.title ?? "(untitled)"}${meta ? `\n ${meta}` : ""}${j.posted || j.closes ? `\n Posted ${j.posted ?? "?"} — closes ${j.closes ?? "?"}` : ""}${j.url ? `\n ${j.url}` : ""}`;
|
|
58
58
|
}
|
|
59
|
+
// ---- muxH tools:
|
|
60
|
+
// The one existing tool calls data.usajobs.gov, which has required an API key and
|
|
61
|
+
// a registered email address since 2023. These two need neither: they read the
|
|
62
|
+
// Federal Register's own public API, which is where the rules that decide who is
|
|
63
|
+
// eligible for a federal job are written down. A job search answers what is open;
|
|
64
|
+
// these answer who is hiring, under what rules, and how that is shifting.
|
|
65
|
+
const UJ_FR = "https://www.federalregister.gov/api/v1";
|
|
66
|
+
const UJ_UA = "mcfentmen-usajobs-mcp/1.2 (contactae2000@gmail.com)";
|
|
67
|
+
async function ujGet(path, label) {
|
|
68
|
+
let res;
|
|
69
|
+
try {
|
|
70
|
+
res = await fetch(`${UJ_FR}${path}`, { headers: { "User-Agent": UJ_UA, Accept: "application/json" }, signal: AbortSignal.timeout(30000) });
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
throw new UsajobsError(`Could not reach the Federal Register: ${e.message}`);
|
|
74
|
+
}
|
|
75
|
+
if (res.status === 403 || res.status === 429)
|
|
76
|
+
throw new UsajobsError(`The Federal Register refused the request (${res.status}); it asks for a descriptive User-Agent carrying a contact address.`);
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
throw new UsajobsError(`${label} returned HTTP ${res.status}.`);
|
|
79
|
+
return (await res.json());
|
|
80
|
+
}
|
|
81
|
+
const ujPlain = (raw) => String(raw ?? "")
|
|
82
|
+
.replace(/<[^>]+>/g, " ")
|
|
83
|
+
.replace(/&/g, "&")
|
|
84
|
+
.replace(/"/g, '"')
|
|
85
|
+
.replace(/'/g, "'")
|
|
86
|
+
.replace(/\s+/g, " ")
|
|
87
|
+
.trim();
|
|
88
|
+
/**
|
|
89
|
+
* The federal employment rules themselves, as published: which document, which
|
|
90
|
+
* agency, what type of rule, when it took effect and what it says. These are the
|
|
91
|
+
* documents that decide who is eligible for a federal post, so a job search that
|
|
92
|
+
* finds an opening says nothing about whether the reader qualifies for it.
|
|
93
|
+
*/
|
|
94
|
+
export async function hiringRules(args) {
|
|
95
|
+
const query = String(args?.query ?? "federal hiring").trim() || "federal hiring";
|
|
96
|
+
const limit = Math.min(Math.max(Number(args?.limit ?? 8) || 8, 1), 30);
|
|
97
|
+
const body = await ujGet(`/documents.json?per_page=${limit}&order=newest&conditions%5Bterm%5D=${encodeURIComponent(query)}`, "the Federal Register search");
|
|
98
|
+
const docs = body.results ?? [];
|
|
99
|
+
if (!docs.length) {
|
|
100
|
+
return `The Federal Register holds no document matching "${query}". The term is matched against titles, abstracts and agency names, so a narrower word finds more.`;
|
|
101
|
+
}
|
|
102
|
+
const lines = [
|
|
103
|
+
`Federal employment rules matching "${query}" — ${body.count?.toLocaleString() ?? "an unstated number of"} document(s) in the register, showing ${docs.length} newest`,
|
|
104
|
+
"",
|
|
105
|
+
...docs.map((doc, i) => {
|
|
106
|
+
const agencies = (doc.agencies ?? []).map((a) => a.name).filter(Boolean).join(", ");
|
|
107
|
+
const abstract = ujPlain(doc.abstract);
|
|
108
|
+
return [
|
|
109
|
+
` ${String(i + 1).padStart(2)}. ${ujPlain(doc.title).slice(0, 84)}`,
|
|
110
|
+
` type: ${doc.type ?? "not reported"}${doc.significant === 1 ? " (significant)" : ""} | agency: ${agencies || "not reported"}`,
|
|
111
|
+
` published ${doc.publication_date ?? "not reported"}${doc.effective_on ? ` | took effect ${doc.effective_on}` : ""} | pages ${doc.page_length ?? "?"}${doc.start_page ? ` from ${doc.start_page}` : ""}`,
|
|
112
|
+
...(doc.topics?.length ? [` topics: ${doc.topics.slice(0, 5).join(", ")}`] : []),
|
|
113
|
+
...(doc.docket_ids?.length ? [` docket: ${doc.docket_ids.slice(0, 2).join(", ")}`] : []),
|
|
114
|
+
...(abstract ? [` ${abstract.slice(0, 260)}${abstract.length > 260 ? " …" : ""}`] : []),
|
|
115
|
+
...(doc.html_url ? [` ${doc.html_url}`] : []),
|
|
116
|
+
].join("\n");
|
|
117
|
+
}),
|
|
118
|
+
];
|
|
119
|
+
const withEffect = docs.filter((d) => d.effective_on).length;
|
|
120
|
+
const significant = docs.filter((d) => d.significant === 1).length;
|
|
121
|
+
lines.push("", `${withEffect} of ${docs.length} carry an effective date, and ${significant} are flagged significant, which is the Register's own signal that a rule is worth reading in full rather than skimming.`, "", "This is the published rulebook, not a job list: a rule here can change who may apply, how long a announcement must stay open, or what a pay band may be, without any posting appearing on USAJobs. The term search runs across full text, so a document can match on a phrase deep in its body; the title is the line to check before assuming it is about hiring.");
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The shape of federal employment activity over time rather than any one document:
|
|
126
|
+
* how many hiring-related rules were published each year, which agency filed them,
|
|
127
|
+
* what kind of rule they were, and which have actually taken effect. A trend is
|
|
128
|
+
* what tells you a policy is moving, and a single search result cannot.
|
|
129
|
+
*/
|
|
130
|
+
export async function hiringSignal(args) {
|
|
131
|
+
const query = String(args?.query ?? "federal hiring").trim() || "federal hiring";
|
|
132
|
+
const years = Math.min(Math.max(Number(args?.years ?? 8) || 8, 2), 30);
|
|
133
|
+
// The register caps one query at 1000 documents and returns them newest first, so
|
|
134
|
+
// the date range is pushed into the query itself; otherwise a wide window would
|
|
135
|
+
// only ever describe the most recent couple of years.
|
|
136
|
+
const cutoff = `${new Date().getUTCFullYear() - years}-01-01`;
|
|
137
|
+
const body = await ujGet(`/documents.json?per_page=1000&order=newest&conditions%5Bterm%5D=${encodeURIComponent(query)}&conditions%5Bpublication_date%5D%5Bgte%5D=${cutoff}`, "the Federal Register search");
|
|
138
|
+
const docs = body.results ?? [];
|
|
139
|
+
if (!docs.length) {
|
|
140
|
+
return `The Federal Register holds no document matching "${query}" published on or after ${cutoff}. The term runs across full text, so a narrower word finds more.`;
|
|
141
|
+
}
|
|
142
|
+
const window = docs;
|
|
143
|
+
const byYear = new Map();
|
|
144
|
+
const byAgency = new Map();
|
|
145
|
+
const byType = new Map();
|
|
146
|
+
let effective = 0;
|
|
147
|
+
let significant = 0;
|
|
148
|
+
for (const doc of window) {
|
|
149
|
+
const year = String(doc.publication_date ?? "").slice(0, 4);
|
|
150
|
+
if (/^\d{4}$/.test(year))
|
|
151
|
+
byYear.set(year, (byYear.get(year) ?? 0) + 1);
|
|
152
|
+
const type = doc.type ?? "not reported";
|
|
153
|
+
byType.set(type, (byType.get(type) ?? 0) + 1);
|
|
154
|
+
if (doc.effective_on)
|
|
155
|
+
effective += 1;
|
|
156
|
+
if (doc.significant === 1)
|
|
157
|
+
significant += 1;
|
|
158
|
+
for (const agency of doc.agencies ?? []) {
|
|
159
|
+
const name = agency.name;
|
|
160
|
+
if (name)
|
|
161
|
+
byAgency.set(name, (byAgency.get(name) ?? 0) + 1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const yearsSeen = [...byYear.keys()].sort();
|
|
165
|
+
const maxYear = Math.max(...byYear.values());
|
|
166
|
+
const covered = yearsSeen.length;
|
|
167
|
+
const truncated = docs.length >= 1000;
|
|
168
|
+
const maxAgency = Math.max(...byAgency.values(), 1);
|
|
169
|
+
const firstYear = yearsSeen[0];
|
|
170
|
+
const lastYear = yearsSeen[yearsSeen.length - 1];
|
|
171
|
+
const firstCount = byYear.get(firstYear) ?? 0;
|
|
172
|
+
const lastCount = byYear.get(lastYear) ?? 0;
|
|
173
|
+
return [
|
|
174
|
+
`Federal employment activity matching "${query}"`,
|
|
175
|
+
` ${body.count?.toLocaleString() ?? "an unstated number of"} document(s) in the register; ${window.length} published in the last ${years} years`,
|
|
176
|
+
` ${covered} calendar year(s) actually represented, from ${firstYear} to ${lastYear}${covered < years ? `, fewer than the ${years} requested` : ""}`,
|
|
177
|
+
...(truncated ? [" this is a capped slice: the register returns at most 1000 documents for one query, so the earliest years in the window are cut off rather than empty"] : []),
|
|
178
|
+
` ${effective} of ${window.length} in the window carry an effective date | ${significant} flagged significant`,
|
|
179
|
+
"",
|
|
180
|
+
"Documents published per year:",
|
|
181
|
+
...yearsSeen.map((y) => {
|
|
182
|
+
const n = byYear.get(y) ?? 0;
|
|
183
|
+
return ` ${y} ${String(n).padStart(4)} ${"#".repeat(Math.max(0, Math.round((n / maxYear) * 34)))}`;
|
|
184
|
+
}),
|
|
185
|
+
"",
|
|
186
|
+
"By document type:",
|
|
187
|
+
...[...byType.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([t, n]) => ` ${t.slice(0, 44).padEnd(44)}${String(n).padStart(4)} ${((n / window.length) * 100).toFixed(0).padStart(3)}%`),
|
|
188
|
+
"",
|
|
189
|
+
"By agency, most active first:",
|
|
190
|
+
...[...byAgency.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([a, n]) => ` ${a.slice(0, 46).padEnd(46)}${String(n).padStart(4)} ${"#".repeat(Math.max(1, Math.round((n / maxAgency) * 20)))}`),
|
|
191
|
+
"",
|
|
192
|
+
`Direction: ${lastYear === firstYear ? "only one year in the window, so no trend to state" : lastCount > firstCount ? `${lastCount} documents in ${lastYear} against ${firstCount} in ${firstYear}, so activity is rising` : lastCount < firstCount ? `${lastCount} documents in ${lastYear} against ${firstCount} in ${firstYear}, so activity is falling` : `the same number in ${firstYear} and ${lastYear}`}`,
|
|
193
|
+
"",
|
|
194
|
+
"An agency is named on a document only if it is the issuing body, so a rule affecting one agency's hiring is counted under that agency and not under the Office of Personnel Management that may have written it. This is publication volume, not activity volume: a long-unimplemented rule is still counted here, which is why the effective-date count is the more useful of the two figures.",
|
|
195
|
+
].join("\n");
|
|
196
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns null when the caller may run the tool, or a short message telling
|
|
3
|
+
* them how to get a key.
|
|
4
|
+
*
|
|
5
|
+
* A good key is remembered for an hour, so a busy session does not hit the
|
|
6
|
+
* license server on every call and keeps working through a brief outage.
|
|
7
|
+
*/
|
|
8
|
+
export declare function premiumRequired(tool: string): Promise<string | null>;
|
package/dist/license.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Premium tools on this server need a license key bought from MCP Marketplace.
|
|
2
|
+
// Buyers put the key in their MCP client config as MCP_LICENSE_KEY. Every tool
|
|
3
|
+
// that is not listed in PREMIUM keeps working without a key.
|
|
4
|
+
const SLUG = "usajobs-mcp";
|
|
5
|
+
// Tools that need a key. Everything else is free.
|
|
6
|
+
const PREMIUM = new Set([
|
|
7
|
+
"hiring_rules",
|
|
8
|
+
]);
|
|
9
|
+
const BUY_URL = `https://mcp-marketplace.io/server/io-github-mrfentmen-${SLUG}`;
|
|
10
|
+
// The license check calls the marketplace's verify endpoint directly instead of
|
|
11
|
+
// using @mcp_marketplace/license. That SDK sends no `apikey` header, so every
|
|
12
|
+
// check it makes is rejected by Supabase before it reaches the function. The
|
|
13
|
+
// publishable key below is public by design - it ships in mcp-marketplace.io's
|
|
14
|
+
// own JavaScript and only ever reaches their licence check.
|
|
15
|
+
const DEFAULT_VERIFY_URL = "https://virupvwhtkpkjsiskckg.supabase.co/functions/v1/verify-key";
|
|
16
|
+
const PUBLISHABLE_KEY = "sb_publishable_BuqlW96Ke8C_zzJG-LQv1Q_YHi_r_4h";
|
|
17
|
+
const OK_CACHE_MS = 60 * 60 * 1000;
|
|
18
|
+
const BAD_CACHE_MS = 60 * 1000;
|
|
19
|
+
const seen = new Map();
|
|
20
|
+
const REASONS = {
|
|
21
|
+
missing_key: "no key is set",
|
|
22
|
+
invalid_format: "that key is not a valid MCP Marketplace key",
|
|
23
|
+
not_found: "that key was not found",
|
|
24
|
+
revoked: "that key has been revoked",
|
|
25
|
+
rotated: "that key was rotated, use your new one",
|
|
26
|
+
expired: "that key has expired, renew it",
|
|
27
|
+
rate_limited: "there were too many checks just now, try again shortly",
|
|
28
|
+
network_error: "the license server could not be reached",
|
|
29
|
+
};
|
|
30
|
+
function blocked(tool, reason) {
|
|
31
|
+
const why = REASONS[reason] ?? `the license server answered "${reason}"`;
|
|
32
|
+
return `"${tool}" needs a license key, but ${why}. ` +
|
|
33
|
+
`Set MCP_LICENSE_KEY in your MCP client config. Get a key: ${BUY_URL}`;
|
|
34
|
+
}
|
|
35
|
+
async function verify(key) {
|
|
36
|
+
const res = await fetch(process.env.MCP_LICENSE_VERIFY_URL || DEFAULT_VERIFY_URL, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
apikey: PUBLISHABLE_KEY,
|
|
40
|
+
Authorization: `Bearer ${PUBLISHABLE_KEY}`,
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({ key, slug: SLUG }),
|
|
44
|
+
// an unusable key answers 400 with a JSON body, so read the body either way
|
|
45
|
+
signal: AbortSignal.timeout(15000),
|
|
46
|
+
});
|
|
47
|
+
const data = (await res.json());
|
|
48
|
+
if (typeof data?.valid !== "boolean")
|
|
49
|
+
return { valid: false, reason: "unexpected_response" };
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Returns null when the caller may run the tool, or a short message telling
|
|
54
|
+
* them how to get a key.
|
|
55
|
+
*
|
|
56
|
+
* A good key is remembered for an hour, so a busy session does not hit the
|
|
57
|
+
* license server on every call and keeps working through a brief outage.
|
|
58
|
+
*/
|
|
59
|
+
export async function premiumRequired(tool) {
|
|
60
|
+
if (!PREMIUM.has(tool))
|
|
61
|
+
return null;
|
|
62
|
+
const key = process.env.MCP_LICENSE_KEY;
|
|
63
|
+
if (!key)
|
|
64
|
+
return blocked(tool, "missing_key");
|
|
65
|
+
const hit = seen.get(key);
|
|
66
|
+
if (hit && Date.now() - hit.at < (hit.valid ? OK_CACHE_MS : BAD_CACHE_MS)) {
|
|
67
|
+
return hit.valid ? null : blocked(tool, hit.reason ?? "invalid");
|
|
68
|
+
}
|
|
69
|
+
let result;
|
|
70
|
+
try {
|
|
71
|
+
result = await verify(key);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
result = { valid: false, reason: "network_error" };
|
|
75
|
+
}
|
|
76
|
+
seen.set(key, { ...result, at: Date.now() });
|
|
77
|
+
return result.valid ? null : blocked(tool, result.reason ?? "invalid");
|
|
78
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { formatJob, searchJobs, UsajobsError } from "./api.js";
|
|
3
|
+
import { formatJob, searchJobs, UsajobsError, hiringRules, hiringSignal } from "./api.js";
|
|
4
|
+
import { premiumRequired } from "./license.js";
|
|
4
5
|
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
6
|
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
7
|
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
@@ -31,6 +32,43 @@ export function createServer() {
|
|
|
31
32
|
return textError(errorMessage(e));
|
|
32
33
|
}
|
|
33
34
|
});
|
|
35
|
+
server.registerTool("hiring_rules", {
|
|
36
|
+
title: "Hiring rules",
|
|
37
|
+
description: "The federal employment rules themselves from the Federal Register's public API: title, rule type, issuing agency, publication and effective dates, page count, dockets and an abstract extract. Says who qualifies for a federal post, which a job search does not. No USAJOBS_API_KEY needed.",
|
|
38
|
+
inputSchema: z.object({
|
|
39
|
+
query: z.string().describe("Term to search the register for, e.g. 'federal hiring', 'merit promotion', 'pay retention'. Defaults to 'federal hiring'.").optional(),
|
|
40
|
+
limit: z.number().describe("Documents listed, 1-30. Defaults to 8.").optional(),
|
|
41
|
+
}),
|
|
42
|
+
annotations: READ_ONLY,
|
|
43
|
+
}, async (args) => {
|
|
44
|
+
// ---- paywall: hiring_rules ----
|
|
45
|
+
const paywallMessage = await premiumRequired("hiring_rules");
|
|
46
|
+
if (paywallMessage)
|
|
47
|
+
return { content: [{ type: "text", text: paywallMessage }], isError: true };
|
|
48
|
+
// ---- paywall: end ----
|
|
49
|
+
try {
|
|
50
|
+
return text(await hiringRules(args));
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
return textError(`Error: ${e.message}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
server.registerTool("hiring_signal", {
|
|
57
|
+
title: "Hiring signal",
|
|
58
|
+
description: "The trend behind those rules rather than any one of them: hiring-related documents published per year, by rule type and by issuing agency, how many have taken effect, and whether activity is rising or falling. No USAJOBS_API_KEY needed.",
|
|
59
|
+
inputSchema: z.object({
|
|
60
|
+
query: z.string().describe("Term to profile. Defaults to 'federal hiring'.").optional(),
|
|
61
|
+
years: z.number().describe("Window in years, 2-30. Defaults to 8.").optional(),
|
|
62
|
+
}),
|
|
63
|
+
annotations: READ_ONLY,
|
|
64
|
+
}, async (args) => {
|
|
65
|
+
try {
|
|
66
|
+
return text(await hiringSignal(args));
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return textError(`Error: ${e.message}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
34
72
|
return server;
|
|
35
73
|
}
|
|
36
74
|
function errorMessage(e) {
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usajobs-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "MCP server for USAJOBS: federal job search. Needs a free API key.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"mcpName": "io.github.mrfentmen/usajobs-mcp",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/mrfentmen/
|
|
9
|
+
"url": "https://github.com/mrfentmen/premium-mcps.git"
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
12
|
"usajobs-mcp": "./dist/index.js"
|