@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.
Files changed (3) hide show
  1. package/README.md +32 -0
  2. package/dist/index.js +84 -51
  3. 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.0';
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
- 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 ?? [];
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 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;
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
- else {
118
- params.set('limit', String(max));
119
- const data = await apiGet(`/v1/courses/search?${params.toString()}`);
120
- pool = data.courses ?? [];
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(console.error);
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.1",
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 run build"
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
  }