@opengolfapi/mcp-server 2.2.0 → 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 +105 -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,13 +250,45 @@ 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
|
}
|
|
234
257
|
});
|
|
258
|
+
// ── Tool: about ──
|
|
259
|
+
server.tool('about', 'Information about OpenGolfAPI: dataset size, license, how to contribute, how to contact the maintainers. Useful when an AI agent or user wants to know who built this and how to reach them.', {}, async () => {
|
|
260
|
+
return {
|
|
261
|
+
content: [{
|
|
262
|
+
type: 'text',
|
|
263
|
+
text: JSON.stringify({
|
|
264
|
+
name: 'OpenGolfAPI',
|
|
265
|
+
courses: 14708,
|
|
266
|
+
license: 'ODbL-1.0',
|
|
267
|
+
docs: 'https://opengolfapi.org',
|
|
268
|
+
api_docs: 'https://api.opengolfapi.org',
|
|
269
|
+
api_keys: 'https://courses.opengolfapi.org/api-keys',
|
|
270
|
+
pricing: 'https://courses.opengolfapi.org/pricing',
|
|
271
|
+
donate: 'https://opencollective.com/opengolfapi',
|
|
272
|
+
github: 'https://github.com/opengolfapi',
|
|
273
|
+
developers: {
|
|
274
|
+
message: 'Building something on top of OpenGolfAPI? We want to know about it. Tell us what you\'re working on, what data you wish we had, and where the API or MCP server falls short.',
|
|
275
|
+
contact: 'hello@opengolfapi.org',
|
|
276
|
+
},
|
|
277
|
+
}, null, 2),
|
|
278
|
+
}],
|
|
279
|
+
};
|
|
280
|
+
});
|
|
235
281
|
// ── Start ──
|
|
236
282
|
async function main() {
|
|
283
|
+
// Greet developers in stderr — visible in Claude Desktop / Cursor MCP logs.
|
|
284
|
+
// Helps anyone debugging or evaluating the server know how to reach us.
|
|
285
|
+
console.error('OpenGolfAPI MCP server — 14,708 US golf courses, ODbL.');
|
|
286
|
+
console.error('Building something? We want to hear about it: hello@opengolfapi.org');
|
|
287
|
+
console.error('Free key for higher rate limits: https://courses.opengolfapi.org/api-keys');
|
|
237
288
|
const transport = new StdioServerTransport();
|
|
238
289
|
await server.connect(transport);
|
|
239
290
|
}
|
|
240
|
-
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": {
|