usajobs-mcp 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/README.md +25 -0
- package/dist/api.d.ts +17 -0
- package/dist/api.js +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +42 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# USAJOBS MCP
|
|
2
|
+
|
|
3
|
+
MCP server for USAJOBS: federal job search. Needs a free API key.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Get a key at https://developer.usajobs.gov/ (free), then:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
export USAJOBS_API_KEY=your_key_here
|
|
11
|
+
export USAJOBS_EMAIL=you@example.com # required contact in User-Agent
|
|
12
|
+
npm install
|
|
13
|
+
npm run build
|
|
14
|
+
node dist/index.js
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The server uses stdio, so it can be connected to Claude Desktop, Cursor, VS Code, MCP Inspector, or another compatible MCP client.
|
|
18
|
+
|
|
19
|
+
## Tools at a glance
|
|
20
|
+
|
|
21
|
+
- `search_jobs`: Keyword + location with pay bands and closing dates.
|
|
22
|
+
|
|
23
|
+
## Limits and privacy
|
|
24
|
+
|
|
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.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class UsajobsError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export interface FedJob {
|
|
4
|
+
title?: string;
|
|
5
|
+
agency?: string;
|
|
6
|
+
locations: string[];
|
|
7
|
+
pay?: string;
|
|
8
|
+
grade?: string;
|
|
9
|
+
posted?: string;
|
|
10
|
+
closes?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function searchJobs(keyword: string, location?: string, limit?: number): Promise<{
|
|
14
|
+
total: number;
|
|
15
|
+
jobs: FedJob[];
|
|
16
|
+
}>;
|
|
17
|
+
export declare function formatJob(j: FedJob, index?: number): string;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* USAJOBS API client. Needs USAJOBS_API_KEY (free at https://developer.usajobs.gov/)
|
|
3
|
+
* plus a contact email in the User-Agent header (USAJOBS_EMAIL).
|
|
4
|
+
* Docs: https://developer.usajobs.gov/
|
|
5
|
+
*/
|
|
6
|
+
const BASE = "https://data.usajobs.gov/api";
|
|
7
|
+
export class UsajobsError extends Error {
|
|
8
|
+
}
|
|
9
|
+
function headers() {
|
|
10
|
+
const key = process.env.USAJOBS_API_KEY;
|
|
11
|
+
const email = process.env.USAJOBS_EMAIL ?? "mcp@example.com";
|
|
12
|
+
if (!key)
|
|
13
|
+
throw new UsajobsError("Set USAJOBS_API_KEY first (free at developer.usajobs.gov).");
|
|
14
|
+
return { "User-Agent": email, Accept: "application/json", "Authorization-Key": key };
|
|
15
|
+
}
|
|
16
|
+
async function getJson(path) {
|
|
17
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
18
|
+
headers: headers(),
|
|
19
|
+
signal: AbortSignal.timeout(20000),
|
|
20
|
+
});
|
|
21
|
+
if (res.status === 401 || res.status === 403)
|
|
22
|
+
throw new UsajobsError("USAJOBS refused (401/403). Check API key.");
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
throw new UsajobsError(`USAJOBS error ${res.status}`);
|
|
25
|
+
return (await res.json());
|
|
26
|
+
}
|
|
27
|
+
export async function searchJobs(keyword, location = "", limit = 5) {
|
|
28
|
+
const qs = new URLSearchParams({ Keyword: keyword.trim(), ResultsPerPage: String(Math.min(Math.max(limit, 1), 50)) });
|
|
29
|
+
if (location.trim())
|
|
30
|
+
qs.set("LocationName", location.trim());
|
|
31
|
+
const data = await getJson(`/search?${qs}`);
|
|
32
|
+
const sr = data.SearchResult ?? {};
|
|
33
|
+
const rows = Array.isArray(sr.SearchResultItems) ? sr.SearchResultItems : [];
|
|
34
|
+
const total = Number(sr.SearchResultCount ?? rows.length);
|
|
35
|
+
return {
|
|
36
|
+
total: isFinite(total) ? total : rows.length,
|
|
37
|
+
jobs: rows.slice(0, limit).map((it) => {
|
|
38
|
+
const d = it.MatchedObjectDescriptor ?? {};
|
|
39
|
+
const pay = d.PositionRemuneration?.[0] ?? {};
|
|
40
|
+
const locs = Array.isArray(d.PositionLocation) ? d.PositionLocation : [];
|
|
41
|
+
return {
|
|
42
|
+
title: d.PositionTitle,
|
|
43
|
+
agency: d.OrganizationName,
|
|
44
|
+
locations: locs.map((l) => String(l.LocationName ?? l.CityName ?? "")).filter(Boolean).slice(0, 3),
|
|
45
|
+
pay: pay.MinimumRange && pay.MaximumRange ? `$${pay.MinimumRange}-$${pay.MaximumRange}` : undefined,
|
|
46
|
+
grade: Array.isArray(d.JobGrade) ? d.JobGrade.map((g) => g.Code).join(",") : undefined,
|
|
47
|
+
posted: d.PublicationStartDate ? String(d.PublicationStartDate).slice(0, 10) : undefined,
|
|
48
|
+
closes: d.ApplicationCloseDate ? String(d.ApplicationCloseDate).slice(0, 10) : undefined,
|
|
49
|
+
url: d.PositionURI,
|
|
50
|
+
};
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function formatJob(j, index) {
|
|
55
|
+
const prefix = index !== undefined ? `${index + 1}. ` : "";
|
|
56
|
+
const meta = [j.agency, j.locations.join("/"), j.pay, j.grade ? `GS-${j.grade}` : ""].filter(Boolean).join(" · ");
|
|
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
const transport = new StdioServerTransport();
|
|
6
|
+
await server.connect(transport);
|
|
7
|
+
console.error("MCP server running on stdio");
|
|
8
|
+
}
|
|
9
|
+
main().catch((err) => {
|
|
10
|
+
console.error("Fatal error:", err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { formatJob, searchJobs, UsajobsError } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
|
+
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
7
|
+
export function createServer() {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "usajobs-mcp",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
});
|
|
12
|
+
server.registerTool("search_jobs", {
|
|
13
|
+
title: "Search federal jobs",
|
|
14
|
+
description: "US federal jobs by keyword and location, with pay bands and closing dates.",
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
keyword: z.string().describe("Keyword, e.g. 'data scientist', 'park ranger'"),
|
|
17
|
+
location: z.string().default("").describe("Location, e.g. 'Washington, DC', empty = anywhere"),
|
|
18
|
+
limit: z.number().int().min(1).max(50).default(5),
|
|
19
|
+
}),
|
|
20
|
+
annotations: READ_ONLY,
|
|
21
|
+
}, async ({ keyword, location, limit }) => {
|
|
22
|
+
try {
|
|
23
|
+
if (!keyword.trim())
|
|
24
|
+
return textError("Error: keyword is empty.");
|
|
25
|
+
const { total, jobs } = await searchJobs(keyword, location, limit);
|
|
26
|
+
if (jobs.length === 0)
|
|
27
|
+
return text("No federal jobs found.");
|
|
28
|
+
return text(`${total.toLocaleString()} federal jobs (showing ${jobs.length}):\n\n${jobs.map((j, i) => formatJob(j, i)).join("\n\n")}`);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return textError(errorMessage(e));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return server;
|
|
35
|
+
}
|
|
36
|
+
function errorMessage(e) {
|
|
37
|
+
if (e instanceof UsajobsError)
|
|
38
|
+
return `Error: ${e.message}`;
|
|
39
|
+
if (e instanceof Error)
|
|
40
|
+
return `Error: ${e.message}`;
|
|
41
|
+
return `Error: ${String(e)}`;
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "usajobs-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for USAJOBS: federal job search. Needs a free API key.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/usajobs-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/mrfentmen/awesome-mcps.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"usajobs-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"dev": "rm -rf dist && tsc -p tsconfig.json && node dist/index.js",
|
|
22
|
+
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"usajobs",
|
|
27
|
+
"jobs",
|
|
28
|
+
"api"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
33
|
+
"zod": "^3.23.8"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"typescript": "^5.6.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
}
|
|
42
|
+
}
|