@opengolfapi/mcp-server 2.2.1 → 2.2.3
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 +32 -0
- package/dist/index.js +84 -51
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -61,6 +61,38 @@ 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
|
+
This server reports unhandled errors to the OpenGolfAPI Sentry project by default so the maintainers can catch bugs that hit real users. **No request bodies, no API keys, no PII** — only stack traces of exceptions thrown inside the server process. Quotas and sampling live in the Sentry project config; the DSN is an ingest endpoint, not a secret.
|
|
67
|
+
|
|
68
|
+
To opt out completely:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
OPENGOLFAPI_DISABLE_TELEMETRY=1 npx @opengolfapi/mcp-server
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
To send to your own Sentry project instead (overrides the default DSN):
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
SENTRY_DSN=https://your-dsn@sentry.io/... npx @opengolfapi/mcp-server
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Example MCP config:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"opengolfapi": {
|
|
86
|
+
"command": "npx",
|
|
87
|
+
"args": ["@opengolfapi/mcp-server"],
|
|
88
|
+
"env": {
|
|
89
|
+
"OPENGOLFAPI_KEY": "ogapi_yourkeyhere"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
64
96
|
## License
|
|
65
97
|
|
|
66
98
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -11,11 +11,30 @@
|
|
|
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.
|
|
16
|
+
//
|
|
17
|
+
// Default DSN ships baked-in so we get bug telemetry from every install — a
|
|
18
|
+
// Sentry DSN is designed to be public (it's an ingest endpoint, not a secret).
|
|
19
|
+
// Users can override with SENTRY_DSN, or opt out completely with
|
|
20
|
+
// OPENGOLFAPI_DISABLE_TELEMETRY=1.
|
|
21
|
+
import * as Sentry from '@sentry/node';
|
|
22
|
+
const DEFAULT_SENTRY_DSN = 'https://2cb261cd86bbfe9e105309d3c2edbced@o4511071885000704.ingest.us.sentry.io/4511345201315840';
|
|
23
|
+
const SENTRY_DSN_ACTIVE = process.env.OPENGOLFAPI_DISABLE_TELEMETRY
|
|
24
|
+
? ''
|
|
25
|
+
: (process.env.SENTRY_DSN || DEFAULT_SENTRY_DSN);
|
|
26
|
+
if (SENTRY_DSN_ACTIVE) {
|
|
27
|
+
Sentry.init({
|
|
28
|
+
dsn: SENTRY_DSN_ACTIVE,
|
|
29
|
+
tracesSampleRate: 0.1,
|
|
30
|
+
release: `opengolfapi-mcp-server@${process.env.npm_package_version || 'unknown'}`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
14
33
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
34
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
16
35
|
import { z } from 'zod';
|
|
17
36
|
// Package version — used in User-Agent so the API can identify MCP traffic.
|
|
18
|
-
const PKG_VERSION = '2.2.
|
|
37
|
+
const PKG_VERSION = '2.2.3';
|
|
19
38
|
const API_BASE = process.env.OPENGOLFAPI_BASE ?? 'https://api.opengolfapi.org';
|
|
20
39
|
// Optional API key for higher rate limits. Anonymous (no key) still works
|
|
21
40
|
// at 1k req/day per IP. With a free key from courses.opengolfapi.org/api-keys,
|
|
@@ -76,61 +95,68 @@ server.tool('search_courses', 'Search golf courses by name, state, or location.
|
|
|
76
95
|
state: z.string().optional().describe('2-letter US state code'),
|
|
77
96
|
limit: z.number().optional().default(10).describe('Max results'),
|
|
78
97
|
}, 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
|
-
|
|
98
|
+
try {
|
|
99
|
+
const max = limit ?? 10;
|
|
100
|
+
const r = radius_mi ?? 25;
|
|
101
|
+
// Build a server-side query. The public search supports `q`, `state`,
|
|
102
|
+
// and `limit`. Geo (lat/lng/radius_mi) is filtered client-side after
|
|
103
|
+
// fetch — see https://github.com/opengolfapi/api/issues for tracking
|
|
104
|
+
// native geo search support.
|
|
105
|
+
const params = new URLSearchParams();
|
|
106
|
+
if (q)
|
|
107
|
+
params.set('q', q);
|
|
108
|
+
if (state)
|
|
109
|
+
params.set('state', state.toUpperCase());
|
|
110
|
+
let pool = [];
|
|
111
|
+
let geoFilterApplied = false;
|
|
112
|
+
if (lat !== undefined && lng !== undefined) {
|
|
113
|
+
// Geo filtering: pull a wider net so the local bbox filter has data
|
|
114
|
+
// to chew on. If a state is given, prefer the state listing
|
|
115
|
+
// (returns up to 100 per page) since it's higher recall than search.
|
|
116
|
+
params.set('limit', '100');
|
|
117
|
+
if (state && !q) {
|
|
118
|
+
const data = await apiGet(`/v1/courses/state/${state.toUpperCase()}`);
|
|
119
|
+
pool = data.courses ?? [];
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const data = await apiGet(`/v1/courses/search?${params.toString()}`);
|
|
123
|
+
pool = data.courses ?? [];
|
|
124
|
+
}
|
|
125
|
+
const latDelta = r / 69.0;
|
|
126
|
+
const lngDelta = r / (69.0 * Math.cos(lat * Math.PI / 180));
|
|
127
|
+
pool = pool.filter(c => {
|
|
128
|
+
if (c.latitude == null || c.longitude == null)
|
|
129
|
+
return false;
|
|
130
|
+
return c.latitude >= lat - latDelta
|
|
131
|
+
&& c.latitude <= lat + latDelta
|
|
132
|
+
&& c.longitude >= lng - lngDelta
|
|
133
|
+
&& c.longitude <= lng + lngDelta;
|
|
134
|
+
});
|
|
135
|
+
geoFilterApplied = true;
|
|
100
136
|
}
|
|
101
137
|
else {
|
|
138
|
+
params.set('limit', String(max));
|
|
102
139
|
const data = await apiGet(`/v1/courses/search?${params.toString()}`);
|
|
103
140
|
pool = data.courses ?? [];
|
|
104
141
|
}
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
142
|
+
const courses = pool.slice(0, max).map(summarizeCourse);
|
|
143
|
+
return {
|
|
144
|
+
content: [{
|
|
145
|
+
type: 'text',
|
|
146
|
+
text: JSON.stringify({
|
|
147
|
+
courses,
|
|
148
|
+
total: courses.length,
|
|
149
|
+
...(geoFilterApplied ? { geo_filter: { lat, lng, radius_mi: r } } : {}),
|
|
150
|
+
source: SOURCE,
|
|
151
|
+
}, null, 2),
|
|
152
|
+
}],
|
|
153
|
+
};
|
|
116
154
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
155
|
+
catch (err) {
|
|
156
|
+
Sentry.captureException(err, { tags: { tool: 'search_courses' } });
|
|
157
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
158
|
+
return { content: [{ type: 'text', text: `Error: ${msg}` }] };
|
|
121
159
|
}
|
|
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
160
|
});
|
|
135
161
|
// ── Tool: get_course ──
|
|
136
162
|
server.tool('get_course', 'Get detailed golf course info including full scorecard with par and handicap index per hole. ODbL licensed.', {
|
|
@@ -144,7 +170,8 @@ server.tool('get_course', 'Get detailed golf course info including full scorecar
|
|
|
144
170
|
apiGet(`/v1/courses/${course_id}/holes`),
|
|
145
171
|
]);
|
|
146
172
|
}
|
|
147
|
-
catch {
|
|
173
|
+
catch (err) {
|
|
174
|
+
Sentry.captureException(err, { tags: { tool: 'get_course' } });
|
|
148
175
|
return { content: [{ type: 'text', text: 'Course not found' }] };
|
|
149
176
|
}
|
|
150
177
|
const scorecard = (holes ?? [])
|
|
@@ -187,6 +214,7 @@ server.tool('get_tees', 'Get all tee sets for a course including ratings, slopes
|
|
|
187
214
|
};
|
|
188
215
|
}
|
|
189
216
|
catch (err) {
|
|
217
|
+
Sentry.captureException(err, { tags: { tool: 'get_tees' } });
|
|
190
218
|
const msg = err instanceof Error ? err.message : String(err);
|
|
191
219
|
return { content: [{ type: 'text', text: `Error: ${msg}` }] };
|
|
192
220
|
}
|
|
@@ -205,6 +233,7 @@ server.tool('get_climate', 'Get monthly climate normals for a course (temperatur
|
|
|
205
233
|
};
|
|
206
234
|
}
|
|
207
235
|
catch (err) {
|
|
236
|
+
Sentry.captureException(err, { tags: { tool: 'get_climate' } });
|
|
208
237
|
const msg = err instanceof Error ? err.message : String(err);
|
|
209
238
|
return { content: [{ type: 'text', text: `Error: ${msg}` }] };
|
|
210
239
|
}
|
|
@@ -228,6 +257,7 @@ server.tool('get_nearby', 'Get nearby points of interest for a course (hotels, r
|
|
|
228
257
|
};
|
|
229
258
|
}
|
|
230
259
|
catch (err) {
|
|
260
|
+
Sentry.captureException(err, { tags: { tool: 'get_nearby' } });
|
|
231
261
|
const msg = err instanceof Error ? err.message : String(err);
|
|
232
262
|
return { content: [{ type: 'text', text: `Error: ${msg}` }] };
|
|
233
263
|
}
|
|
@@ -265,4 +295,7 @@ async function main() {
|
|
|
265
295
|
const transport = new StdioServerTransport();
|
|
266
296
|
await server.connect(transport);
|
|
267
297
|
}
|
|
268
|
-
main().catch(
|
|
298
|
+
main().catch((err) => {
|
|
299
|
+
Sentry.captureException(err);
|
|
300
|
+
console.error(err);
|
|
301
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengolfapi/mcp-server",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
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": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc",
|
|
16
|
-
"prepublishOnly": "npm
|
|
16
|
+
"prepublishOnly": "npm test",
|
|
17
|
+
"test": "npm run build && vitest run"
|
|
17
18
|
},
|
|
18
19
|
"repository": {
|
|
19
20
|
"type": "git",
|
|
@@ -33,10 +34,12 @@
|
|
|
33
34
|
"homepage": "https://opengolfapi.org",
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
37
|
+
"@sentry/node": "^10.51.0",
|
|
36
38
|
"zod": "^3.22.0"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@types/node": "^20.0.0",
|
|
40
|
-
"typescript": "^5.0.0"
|
|
42
|
+
"typescript": "^5.0.0",
|
|
43
|
+
"vitest": "^4.1.5"
|
|
41
44
|
}
|
|
42
45
|
}
|