@opengolfapi/mcp-server 2.2.1 → 2.2.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 +24 -0
- package/dist/index.js +77 -51
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -61,6 +61,30 @@ Donor tiers raise the daily limit further (10k / 50k / 250k / 1M).
|
|
|
61
61
|
- `get_climate(id)` — monthly climate normals for the course location
|
|
62
62
|
- `get_nearby(id)` — nearby POIs (hotels, restaurants, airports)
|
|
63
63
|
|
|
64
|
+
## Telemetry
|
|
65
|
+
|
|
66
|
+
Optional: set `SENTRY_DSN` env var to send errors to your own Sentry instance.
|
|
67
|
+
We do not collect telemetry from end users — this is opt-in for the operator
|
|
68
|
+
running the server.
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"opengolfapi": {
|
|
74
|
+
"command": "npx",
|
|
75
|
+
"args": ["@opengolfapi/mcp-server"],
|
|
76
|
+
"env": {
|
|
77
|
+
"OPENGOLFAPI_KEY": "ogapi_yourkeyhere",
|
|
78
|
+
"SENTRY_DSN": "https://<key>@o<org>.ingest.sentry.io/<project>"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
When `SENTRY_DSN` is unset the SDK is a complete no-op — nothing is initialized
|
|
86
|
+
and no network calls are made.
|
|
87
|
+
|
|
64
88
|
## License
|
|
65
89
|
|
|
66
90
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -11,11 +11,23 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Install: npx @opengolfapi/mcp-server
|
|
13
13
|
*/
|
|
14
|
+
// Sentry must initialize BEFORE any other imports that could throw, so any
|
|
15
|
+
// load-time error in transitive deps still gets captured. The init is a no-op
|
|
16
|
+
// when SENTRY_DSN is unset, so end users who run the server via npx never
|
|
17
|
+
// send telemetry unless they set the env var themselves.
|
|
18
|
+
import * as Sentry from '@sentry/node';
|
|
19
|
+
if (process.env.SENTRY_DSN) {
|
|
20
|
+
Sentry.init({
|
|
21
|
+
dsn: process.env.SENTRY_DSN,
|
|
22
|
+
tracesSampleRate: 0.1,
|
|
23
|
+
release: `opengolfapi-mcp-server@${process.env.npm_package_version || 'unknown'}`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
14
26
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
27
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
16
28
|
import { z } from 'zod';
|
|
17
29
|
// Package version — used in User-Agent so the API can identify MCP traffic.
|
|
18
|
-
const PKG_VERSION = '2.2.
|
|
30
|
+
const PKG_VERSION = '2.2.2';
|
|
19
31
|
const API_BASE = process.env.OPENGOLFAPI_BASE ?? 'https://api.opengolfapi.org';
|
|
20
32
|
// Optional API key for higher rate limits. Anonymous (no key) still works
|
|
21
33
|
// at 1k req/day per IP. With a free key from courses.opengolfapi.org/api-keys,
|
|
@@ -76,61 +88,68 @@ server.tool('search_courses', 'Search golf courses by name, state, or location.
|
|
|
76
88
|
state: z.string().optional().describe('2-letter US state code'),
|
|
77
89
|
limit: z.number().optional().default(10).describe('Max results'),
|
|
78
90
|
}, async ({ lat, lng, radius_mi, query: q, state, limit }) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
try {
|
|
92
|
+
const max = limit ?? 10;
|
|
93
|
+
const r = radius_mi ?? 25;
|
|
94
|
+
// Build a server-side query. The public search supports `q`, `state`,
|
|
95
|
+
// and `limit`. Geo (lat/lng/radius_mi) is filtered client-side after
|
|
96
|
+
// fetch — see https://github.com/opengolfapi/api/issues for tracking
|
|
97
|
+
// native geo search support.
|
|
98
|
+
const params = new URLSearchParams();
|
|
99
|
+
if (q)
|
|
100
|
+
params.set('q', q);
|
|
101
|
+
if (state)
|
|
102
|
+
params.set('state', state.toUpperCase());
|
|
103
|
+
let pool = [];
|
|
104
|
+
let geoFilterApplied = false;
|
|
105
|
+
if (lat !== undefined && lng !== undefined) {
|
|
106
|
+
// Geo filtering: pull a wider net so the local bbox filter has data
|
|
107
|
+
// to chew on. If a state is given, prefer the state listing
|
|
108
|
+
// (returns up to 100 per page) since it's higher recall than search.
|
|
109
|
+
params.set('limit', '100');
|
|
110
|
+
if (state && !q) {
|
|
111
|
+
const data = await apiGet(`/v1/courses/state/${state.toUpperCase()}`);
|
|
112
|
+
pool = data.courses ?? [];
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const data = await apiGet(`/v1/courses/search?${params.toString()}`);
|
|
116
|
+
pool = data.courses ?? [];
|
|
117
|
+
}
|
|
118
|
+
const latDelta = r / 69.0;
|
|
119
|
+
const lngDelta = r / (69.0 * Math.cos(lat * Math.PI / 180));
|
|
120
|
+
pool = pool.filter(c => {
|
|
121
|
+
if (c.latitude == null || c.longitude == null)
|
|
122
|
+
return false;
|
|
123
|
+
return c.latitude >= lat - latDelta
|
|
124
|
+
&& c.latitude <= lat + latDelta
|
|
125
|
+
&& c.longitude >= lng - lngDelta
|
|
126
|
+
&& c.longitude <= lng + lngDelta;
|
|
127
|
+
});
|
|
128
|
+
geoFilterApplied = true;
|
|
100
129
|
}
|
|
101
130
|
else {
|
|
131
|
+
params.set('limit', String(max));
|
|
102
132
|
const data = await apiGet(`/v1/courses/search?${params.toString()}`);
|
|
103
133
|
pool = data.courses ?? [];
|
|
104
134
|
}
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
const courses = pool.slice(0, max).map(summarizeCourse);
|
|
136
|
+
return {
|
|
137
|
+
content: [{
|
|
138
|
+
type: 'text',
|
|
139
|
+
text: JSON.stringify({
|
|
140
|
+
courses,
|
|
141
|
+
total: courses.length,
|
|
142
|
+
...(geoFilterApplied ? { geo_filter: { lat, lng, radius_mi: r } } : {}),
|
|
143
|
+
source: SOURCE,
|
|
144
|
+
}, null, 2),
|
|
145
|
+
}],
|
|
146
|
+
};
|
|
116
147
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
148
|
+
catch (err) {
|
|
149
|
+
Sentry.captureException(err, { tags: { tool: 'search_courses' } });
|
|
150
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
+
return { content: [{ type: 'text', text: `Error: ${msg}` }] };
|
|
121
152
|
}
|
|
122
|
-
const courses = pool.slice(0, max).map(summarizeCourse);
|
|
123
|
-
return {
|
|
124
|
-
content: [{
|
|
125
|
-
type: 'text',
|
|
126
|
-
text: JSON.stringify({
|
|
127
|
-
courses,
|
|
128
|
-
total: courses.length,
|
|
129
|
-
...(geoFilterApplied ? { geo_filter: { lat, lng, radius_mi: r } } : {}),
|
|
130
|
-
source: SOURCE,
|
|
131
|
-
}, null, 2),
|
|
132
|
-
}],
|
|
133
|
-
};
|
|
134
153
|
});
|
|
135
154
|
// ── Tool: get_course ──
|
|
136
155
|
server.tool('get_course', 'Get detailed golf course info including full scorecard with par and handicap index per hole. ODbL licensed.', {
|
|
@@ -144,7 +163,8 @@ server.tool('get_course', 'Get detailed golf course info including full scorecar
|
|
|
144
163
|
apiGet(`/v1/courses/${course_id}/holes`),
|
|
145
164
|
]);
|
|
146
165
|
}
|
|
147
|
-
catch {
|
|
166
|
+
catch (err) {
|
|
167
|
+
Sentry.captureException(err, { tags: { tool: 'get_course' } });
|
|
148
168
|
return { content: [{ type: 'text', text: 'Course not found' }] };
|
|
149
169
|
}
|
|
150
170
|
const scorecard = (holes ?? [])
|
|
@@ -187,6 +207,7 @@ server.tool('get_tees', 'Get all tee sets for a course including ratings, slopes
|
|
|
187
207
|
};
|
|
188
208
|
}
|
|
189
209
|
catch (err) {
|
|
210
|
+
Sentry.captureException(err, { tags: { tool: 'get_tees' } });
|
|
190
211
|
const msg = err instanceof Error ? err.message : String(err);
|
|
191
212
|
return { content: [{ type: 'text', text: `Error: ${msg}` }] };
|
|
192
213
|
}
|
|
@@ -205,6 +226,7 @@ server.tool('get_climate', 'Get monthly climate normals for a course (temperatur
|
|
|
205
226
|
};
|
|
206
227
|
}
|
|
207
228
|
catch (err) {
|
|
229
|
+
Sentry.captureException(err, { tags: { tool: 'get_climate' } });
|
|
208
230
|
const msg = err instanceof Error ? err.message : String(err);
|
|
209
231
|
return { content: [{ type: 'text', text: `Error: ${msg}` }] };
|
|
210
232
|
}
|
|
@@ -228,6 +250,7 @@ server.tool('get_nearby', 'Get nearby points of interest for a course (hotels, r
|
|
|
228
250
|
};
|
|
229
251
|
}
|
|
230
252
|
catch (err) {
|
|
253
|
+
Sentry.captureException(err, { tags: { tool: 'get_nearby' } });
|
|
231
254
|
const msg = err instanceof Error ? err.message : String(err);
|
|
232
255
|
return { content: [{ type: 'text', text: `Error: ${msg}` }] };
|
|
233
256
|
}
|
|
@@ -265,4 +288,7 @@ async function main() {
|
|
|
265
288
|
const transport = new StdioServerTransport();
|
|
266
289
|
await server.connect(transport);
|
|
267
290
|
}
|
|
268
|
-
main().catch(
|
|
291
|
+
main().catch((err) => {
|
|
292
|
+
Sentry.captureException(err);
|
|
293
|
+
console.error(err);
|
|
294
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengolfapi/mcp-server",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Open MCP server for AI agents to query the OpenGolfAPI dataset (14,708 US golf courses, ODbL)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"homepage": "https://opengolfapi.org",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
|
+
"@sentry/node": "^10.51.0",
|
|
36
37
|
"zod": "^3.22.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|