@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.
Files changed (3) hide show
  1. package/README.md +24 -0
  2. package/dist/index.js +105 -51
  3. 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.0';
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
- const max = limit ?? 10;
80
- const r = radius_mi ?? 25;
81
- // Build a server-side query. The public search supports `q`, `state`,
82
- // and `limit`. Geo (lat/lng/radius_mi) is filtered client-side after
83
- // fetch see https://github.com/opengolfapi/api/issues for tracking
84
- // native geo search support.
85
- const params = new URLSearchParams();
86
- if (q)
87
- params.set('q', q);
88
- if (state)
89
- params.set('state', state.toUpperCase());
90
- let pool = [];
91
- let geoFilterApplied = false;
92
- if (lat !== undefined && lng !== undefined) {
93
- // Geo filtering: pull a wider net so the local bbox filter has data
94
- // to chew on. If a state is given, prefer the state listing
95
- // (returns up to 100 per page) since it's higher recall than search.
96
- params.set('limit', '100');
97
- if (state && !q) {
98
- const data = await apiGet(`/v1/courses/state/${state.toUpperCase()}`);
99
- pool = data.courses ?? [];
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 latDelta = r / 69.0;
106
- const lngDelta = r / (69.0 * Math.cos(lat * Math.PI / 180));
107
- pool = pool.filter(c => {
108
- if (c.latitude == null || c.longitude == null)
109
- return false;
110
- return c.latitude >= lat - latDelta
111
- && c.latitude <= lat + latDelta
112
- && c.longitude >= lng - lngDelta
113
- && c.longitude <= lng + lngDelta;
114
- });
115
- geoFilterApplied = true;
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
- else {
118
- params.set('limit', String(max));
119
- const data = await apiGet(`/v1/courses/search?${params.toString()}`);
120
- pool = data.courses ?? [];
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(console.error);
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.0",
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": {