@cliwant/mcp-sam-gov 0.2.1 → 0.3.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/src/grants.ts CHANGED
@@ -1,155 +1,158 @@
1
- /**
2
- * Grants.gov v1 API wrappers (keyless).
3
- *
4
- * Grants.gov hosts federal financial-assistance opportunities (grants,
5
- * cooperative agreements). Distinct from SAM.gov contracts but the
6
- * same pursuit ICP often cares about both.
7
- *
8
- * Endpoints (POST JSON, no key):
9
- * - /v1/api/search2 — search opportunities
10
- * - /v1/api/fetchOpportunity — single grant detail
11
- *
12
- * Documented at https://grants.gov/web/grants/s2s/grantor/schemas/grants-search-2-soap.html
13
- */
14
-
15
- const GRANTS = "https://api.grants.gov/v1/api";
16
-
17
- async function postJson<T>(
18
- endpoint: string,
19
- body: Record<string, unknown>,
20
- ): Promise<T> {
21
- const r = await fetch(`${GRANTS}/${endpoint}`, {
22
- method: "POST",
23
- headers: { "Content-Type": "application/json" },
24
- body: JSON.stringify(body),
25
- signal: AbortSignal.timeout(15_000),
26
- });
27
- if (!r.ok) {
28
- throw new Error(`Grants.gov ${endpoint} returned ${r.status}`);
29
- }
30
- return (await r.json()) as T;
31
- }
32
-
33
- export type GrantStatus = "forecasted" | "posted" | "closed" | "archived";
34
-
35
- export async function searchGrants(args: {
36
- keyword?: string;
37
- cfda?: string; // CFDA program number, e.g. "10.500"
38
- agency?: string; // agency code, e.g. "DHS-FEMA"
39
- oppNum?: string; // opportunity number
40
- oppStatuses?: GrantStatus[];
41
- rows?: number;
42
- }) {
43
- const body: Record<string, unknown> = {
44
- rows: args.rows ?? 10,
45
- keyword: args.keyword ?? "",
46
- cfda: args.cfda ?? "",
47
- agencies: args.agency ?? "",
48
- oppNum: args.oppNum ?? "",
49
- oppStatuses: (args.oppStatuses ?? ["forecasted", "posted"]).join("|"),
50
- };
51
- type Resp = {
52
- errorcode?: number;
53
- msg?: string;
54
- data?: {
55
- hitCount?: number;
56
- oppHits?: {
57
- id?: string;
58
- number?: string;
59
- title?: string;
60
- agencyCode?: string;
61
- agencyName?: string;
62
- openDate?: string;
63
- closeDate?: string;
64
- oppStatus?: string;
65
- docType?: string;
66
- cfdaList?: string;
67
- }[];
68
- };
69
- };
70
- const json = await postJson<Resp>("search2", body);
71
- if (json.errorcode && json.errorcode !== 0) {
72
- throw new Error(`Grants.gov error: ${json.msg ?? "unknown"}`);
73
- }
74
- return {
75
- totalRecords: json.data?.hitCount ?? 0,
76
- grants: (json.data?.oppHits ?? []).map((g) => ({
77
- id: g.id ?? "",
78
- opportunityNumber: g.number ?? "",
79
- title: g.title ?? "",
80
- agencyCode: g.agencyCode ?? "",
81
- agencyName: g.agencyName ?? "",
82
- openDate: g.openDate,
83
- closeDate: g.closeDate,
84
- status: g.oppStatus,
85
- docType: g.docType,
86
- cfdaList: g.cfdaList,
87
- })),
88
- };
89
- }
90
-
91
- export async function getGrant(args: { opportunityId: string }) {
92
- type Resp = {
93
- errorcode?: number;
94
- msg?: string;
95
- data?: {
96
- id?: number;
97
- opportunityNumber?: string;
98
- opportunityTitle?: string;
99
- owningAgencyCode?: string;
100
- synopsisDesc?: string;
101
- synopsis?: {
102
- synopsisDesc?: string;
103
- applicantTypes?: { description?: string }[];
104
- fundingActivityCategories?: { description?: string }[];
105
- fundingInstruments?: { description?: string }[];
106
- responseDate?: string;
107
- postingDate?: string;
108
- archiveDate?: string;
109
- awardCeiling?: number;
110
- awardFloor?: number;
111
- estimatedFunding?: number;
112
- expectedNumberOfAwards?: number;
113
- agencyName?: string;
114
- agencyCode?: string;
115
- };
116
- opportunityHistoryDetails?: { actionType?: string; actionDate?: string }[];
117
- cfdas?: { cfdaNumber?: string; programTitle?: string }[];
118
- };
119
- };
120
- const json = await postJson<Resp>("fetchOpportunity", {
121
- opportunityId: args.opportunityId,
122
- });
123
- if (json.errorcode && json.errorcode !== 0) {
124
- throw new Error(`Grants.gov error: ${json.msg ?? "unknown"}`);
125
- }
126
- const d = json.data ?? {};
127
- const s = d.synopsis ?? {};
128
- return {
129
- id: d.id ?? 0,
130
- opportunityNumber: d.opportunityNumber ?? "",
131
- title: d.opportunityTitle ?? "",
132
- agency: { code: s.agencyCode ?? d.owningAgencyCode, name: s.agencyName },
133
- description: s.synopsisDesc ?? d.synopsisDesc ?? "",
134
- postingDate: s.postingDate,
135
- responseDate: s.responseDate,
136
- archiveDate: s.archiveDate,
137
- awardCeiling: s.awardCeiling,
138
- awardFloor: s.awardFloor,
139
- estimatedFunding: s.estimatedFunding,
140
- expectedNumberOfAwards: s.expectedNumberOfAwards,
141
- applicantTypes: (s.applicantTypes ?? [])
142
- .map((a) => a.description)
143
- .filter(Boolean) as string[],
144
- fundingInstruments: (s.fundingInstruments ?? [])
145
- .map((f) => f.description)
146
- .filter(Boolean) as string[],
147
- fundingCategories: (s.fundingActivityCategories ?? [])
148
- .map((f) => f.description)
149
- .filter(Boolean) as string[],
150
- cfdaPrograms: (d.cfdas ?? []).map((c) => ({
151
- number: c.cfdaNumber ?? "",
152
- title: c.programTitle ?? "",
153
- })),
154
- };
155
- }
1
+ /**
2
+ * Grants.gov v1 API wrappers (keyless).
3
+ *
4
+ * Grants.gov hosts federal financial-assistance opportunities (grants,
5
+ * cooperative agreements). Distinct from SAM.gov contracts but the
6
+ * same pursuit ICP often cares about both.
7
+ *
8
+ * Endpoints (POST JSON, no key):
9
+ * - /v1/api/search2 — search opportunities
10
+ * - /v1/api/fetchOpportunity — single grant detail
11
+ *
12
+ * Documented at https://grants.gov/web/grants/s2s/grantor/schemas/grants-search-2-soap.html
13
+ */
14
+
15
+ import { fetchWithRetry } from "./errors.js";
16
+
17
+ const GRANTS = "https://api.grants.gov/v1/api";
18
+
19
+ async function postJson<T>(
20
+ endpoint: string,
21
+ body: Record<string, unknown>,
22
+ ): Promise<T> {
23
+ const r = await fetchWithRetry(
24
+ `${GRANTS}/${endpoint}`,
25
+ {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify(body),
29
+ signal: AbortSignal.timeout(15_000),
30
+ },
31
+ `grants.gov:${endpoint}`,
32
+ );
33
+ return (await r.json()) as T;
34
+ }
35
+
36
+ export type GrantStatus = "forecasted" | "posted" | "closed" | "archived";
37
+
38
+ export async function searchGrants(args: {
39
+ keyword?: string;
40
+ cfda?: string; // CFDA program number, e.g. "10.500"
41
+ agency?: string; // agency code, e.g. "DHS-FEMA"
42
+ oppNum?: string; // opportunity number
43
+ oppStatuses?: GrantStatus[];
44
+ rows?: number;
45
+ }) {
46
+ const body: Record<string, unknown> = {
47
+ rows: args.rows ?? 10,
48
+ keyword: args.keyword ?? "",
49
+ cfda: args.cfda ?? "",
50
+ agencies: args.agency ?? "",
51
+ oppNum: args.oppNum ?? "",
52
+ oppStatuses: (args.oppStatuses ?? ["forecasted", "posted"]).join("|"),
53
+ };
54
+ type Resp = {
55
+ errorcode?: number;
56
+ msg?: string;
57
+ data?: {
58
+ hitCount?: number;
59
+ oppHits?: {
60
+ id?: string;
61
+ number?: string;
62
+ title?: string;
63
+ agencyCode?: string;
64
+ agencyName?: string;
65
+ openDate?: string;
66
+ closeDate?: string;
67
+ oppStatus?: string;
68
+ docType?: string;
69
+ cfdaList?: string;
70
+ }[];
71
+ };
72
+ };
73
+ const json = await postJson<Resp>("search2", body);
74
+ if (json.errorcode && json.errorcode !== 0) {
75
+ throw new Error(`Grants.gov error: ${json.msg ?? "unknown"}`);
76
+ }
77
+ return {
78
+ totalRecords: json.data?.hitCount ?? 0,
79
+ grants: (json.data?.oppHits ?? []).map((g) => ({
80
+ id: g.id ?? "",
81
+ opportunityNumber: g.number ?? "",
82
+ title: g.title ?? "",
83
+ agencyCode: g.agencyCode ?? "",
84
+ agencyName: g.agencyName ?? "",
85
+ openDate: g.openDate,
86
+ closeDate: g.closeDate,
87
+ status: g.oppStatus,
88
+ docType: g.docType,
89
+ cfdaList: g.cfdaList,
90
+ })),
91
+ };
92
+ }
93
+
94
+ export async function getGrant(args: { opportunityId: string }) {
95
+ type Resp = {
96
+ errorcode?: number;
97
+ msg?: string;
98
+ data?: {
99
+ id?: number;
100
+ opportunityNumber?: string;
101
+ opportunityTitle?: string;
102
+ owningAgencyCode?: string;
103
+ synopsisDesc?: string;
104
+ synopsis?: {
105
+ synopsisDesc?: string;
106
+ applicantTypes?: { description?: string }[];
107
+ fundingActivityCategories?: { description?: string }[];
108
+ fundingInstruments?: { description?: string }[];
109
+ responseDate?: string;
110
+ postingDate?: string;
111
+ archiveDate?: string;
112
+ awardCeiling?: number;
113
+ awardFloor?: number;
114
+ estimatedFunding?: number;
115
+ expectedNumberOfAwards?: number;
116
+ agencyName?: string;
117
+ agencyCode?: string;
118
+ };
119
+ opportunityHistoryDetails?: { actionType?: string; actionDate?: string }[];
120
+ cfdas?: { cfdaNumber?: string; programTitle?: string }[];
121
+ };
122
+ };
123
+ const json = await postJson<Resp>("fetchOpportunity", {
124
+ opportunityId: args.opportunityId,
125
+ });
126
+ if (json.errorcode && json.errorcode !== 0) {
127
+ throw new Error(`Grants.gov error: ${json.msg ?? "unknown"}`);
128
+ }
129
+ const d = json.data ?? {};
130
+ const s = d.synopsis ?? {};
131
+ return {
132
+ id: d.id ?? 0,
133
+ opportunityNumber: d.opportunityNumber ?? "",
134
+ title: d.opportunityTitle ?? "",
135
+ agency: { code: s.agencyCode ?? d.owningAgencyCode, name: s.agencyName },
136
+ description: s.synopsisDesc ?? d.synopsisDesc ?? "",
137
+ postingDate: s.postingDate,
138
+ responseDate: s.responseDate,
139
+ archiveDate: s.archiveDate,
140
+ awardCeiling: s.awardCeiling,
141
+ awardFloor: s.awardFloor,
142
+ estimatedFunding: s.estimatedFunding,
143
+ expectedNumberOfAwards: s.expectedNumberOfAwards,
144
+ applicantTypes: (s.applicantTypes ?? [])
145
+ .map((a) => a.description)
146
+ .filter(Boolean) as string[],
147
+ fundingInstruments: (s.fundingInstruments ?? [])
148
+ .map((f) => f.description)
149
+ .filter(Boolean) as string[],
150
+ fundingCategories: (s.fundingActivityCategories ?? [])
151
+ .map((f) => f.description)
152
+ .filter(Boolean) as string[],
153
+ cfdaPrograms: (d.cfdas ?? []).map((c) => ({
154
+ number: c.cfdaNumber ?? "",
155
+ title: c.programTitle ?? "",
156
+ })),
157
+ };
158
+ }
package/src/server.ts CHANGED
@@ -29,9 +29,10 @@ import * as usas from "./usaspending.js";
29
29
  import * as fedreg from "./federal-register.js";
30
30
  import * as ecfr from "./ecfr.js";
31
31
  import * as grants from "./grants.js";
32
+ import { toToolError } from "./errors.js";
32
33
 
33
34
  const SERVER_NAME = "mcp-sam-gov";
34
- const SERVER_VERSION = "0.2.1";
35
+ const SERVER_VERSION = "0.3.0";
35
36
 
36
37
  // ─── Tool input schemas (Zod) ────────────────────────────────────
37
38
 
@@ -548,16 +549,24 @@ async function main() {
548
549
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
549
550
  const { name, arguments: args } = req.params;
550
551
  try {
551
- const result = await runTool(name, args ?? {}, sam);
552
+ const data = await runTool(name, args ?? {}, sam);
553
+ // Structured success envelope. Calling agent can rely on
554
+ // `ok: true` to know the payload is in `data`.
555
+ const envelope = { ok: true as const, data };
552
556
  return {
553
557
  content: [
554
- { type: "text" as const, text: JSON.stringify(result, null, 2) },
558
+ { type: "text" as const, text: JSON.stringify(envelope, null, 2) },
555
559
  ],
556
560
  };
557
561
  } catch (err) {
558
- const message = err instanceof Error ? err.message : String(err);
562
+ // Structured error envelope. The agent can read `error.kind`
563
+ // and `error.retryable` to decide what to do next.
564
+ const error = toToolError(err, name);
565
+ const envelope = { ok: false as const, error };
559
566
  return {
560
- content: [{ type: "text" as const, text: `Tool error: ${message}` }],
567
+ content: [
568
+ { type: "text" as const, text: JSON.stringify(envelope, null, 2) },
569
+ ],
561
570
  isError: true,
562
571
  };
563
572
  }