Skip to content

Commit 1ec430b

Browse files
committed
2.8.0: free gross scoring + identity-gated dev keys (no id, no key)
+5 tools (22→27): list_game_formats, score_round (gross, keyless — the game is free), create_dev_key + link_dev_key + list_dev_keys (Sign in with OpenGolf → mint/confirm a key bound to your OpenGolf ID). Guard updated: /compute(gross) + /developer are OPEN; the moat (settle/events/geo/ads/surface/stats) stays blocked. README + server.json synced.
1 parent 30d4779 commit 1ec430b

5 files changed

Lines changed: 80 additions & 6 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Get a free `OPENGOLFAPI_KEY` at https://courses.opengolfapi.org/api-keys. Read t
4848
## Tools
4949

5050
<!-- TOOLS:START (auto-generated from src by gen-manifest.mjs — do not edit by hand) -->
51-
_22 tools:_
51+
_27 tools:_
5252

5353
- `search_courses` — Search golf courses by name, state, or location. Returns full course info. ODbL licensed data from OpenGolfAPI.
5454
- `get_course` — Get detailed golf course info including full scorecard with par and handicap index per hole. ODbL licensed.
@@ -65,6 +65,11 @@ _22 tools:_
6565
- `join_tournament` — Join a player into a tournament/round by redeeming its invite token. The player lands in the shared event = part of the field. Handicap is optional: gross by default (no handicap needed); if the player has one it auto-applies for net. Requires OPENGOLFAPI_KEY.
6666
- `request_sign_in_code` — Start "Sign in with OpenGolf" for a player: emails them a 6-digit code. No API key needed — the email authenticates them. They read you the code, then call complete_sign_in.
6767
- `complete_sign_in` — Finish "Sign in with OpenGolf": exchange the player's 6-digit code for their OpenGolf ID access token + portable player_id. Use the returned access_token as the X-OpenGolf-Token header to act AS the player, within whatever scopes you were granted. scopes: space-separated (default "identity").
68+
- `list_game_formats` — List every OpenMatch scoring format (stroke, stableford, match_play, skins, scramble, best_ball, nassau, ctp, longest_drive, greenies, wolf, …) and the free/gated line. GROSS scoring is free + keyless; net/handicap, settlement, strokes-gained & tournament scale are gated. No key needed.
69+
- `score_round` — Score a round in any format — GROSS, FREE, no key. players: [{player_id, holes:{"1":4,...}}] for stroke formats, or entries:[{player_id,hole,value}] for shot formats (ctp/longest_drive/greenies). holes: [{hole,par,stroke_index?}]. Returns standings. NET/handicap is the gated tier (add a compute key); money-safe settlement lives elsewhere.
70+
- `create_dev_key` — Sign up to build: mint an API key bound to your OpenGolf ID. Requires an OpenGolf ID access_token (from complete_sign_in) + the SAME email you signed in with. No OpenGolf ID, no key. Returns the key ONCE. Free scope = read + contribute + keyless gross scoring; net/settlement/geometry/game need an entitled key ([email protected]).
71+
- `link_dev_key` — Confirm your OpenGolf ID on an EXISTING (legacy) key — the simple "add my ID" step. Sign in with the email the key was issued under, then call this with that access_token + the api_key. Verifies + keeps the key and its scopes. (We don't revoke legacy keys — they just confirm their ID.)
72+
- `list_dev_keys` — List the API keys owned by your OpenGolf ID (prefixes only — never the secret). Requires an OpenGolf ID access_token from complete_sign_in.
6873
- `get_profile` — Read a player's public OpenGolf ID card — display name, avatar, links, home course, golf prefs, bucket list. Apps render it as a player card. Requires OPENGOLFAPI_KEY.
6974
- `update_profile` — Edit a player's OpenGolf ID profile — self-asserted fields (display_name, avatar_url, bio, links, home_course_id, bucket_list, preferred_formats…). Derived facts (handicap/stats) cannot be set here. A claimed player needs their grant. Requires OPENGOLFAPI_KEY.
7075
- `get_awards` — A player's OpenAwards — earned trophies (aces/eagles/birdies/broke-X/money/streaks/loyalty, all derived + unfakeable), organizer-granted custom awards, and the course passport (distinct courses played + logos). Requires OPENGOLFAPI_KEY.

gen-manifest.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ const tools = [...src.matchAll(/server\.tool\(\s*'([a-z_]+)',\s*'((?:[^'\\]|\\.)
1818
if (!tools.length) { console.error('gen-manifest: found 0 tools — aborting (regex drift?)'); process.exit(1); }
1919

2020
// CORE DISCIPLINE: nothing paid/gated/geo is ever public. Fail the build if the public MCP calls a gated
21-
// path (compute/events/awards/geo/ads/surface/settle/stats). The public MCP is the OPEN standard ONLY.
21+
// path. The OPEN standard includes GROSS scoring (/compute, free+keyless) + dev signup (/developer); the
22+
// MOAT stays out: settlement, events, geometry/geo, ads, the engine catalog (surface), corpus stats.
2223
const calls = [...src.matchAll(/api(?:Get|Post)[^(]*\(\s*[`'"]([^`'"]+)/g)].map((m) => m[1]);
23-
const gated = calls.filter((p) => /\/(compute|events|geo|ads|surface)\b|settle|players\/[^/]*\/stats/.test(p)); // awards/profile/beacon are OPEN (player features)
24+
const gated = calls.filter((p) => /\/(events|geo|ads|surface)\b|settle|players\/[^/]*\/stats/.test(p)); // compute(gross)/developer/awards/profile/beacon are OPEN
2425
if (gated.length) { console.error('gen-manifest: PUBLIC MCP calls GATED paths — paid/gated must NEVER be public:', gated.join(', '), '\nAborting.'); process.exit(1); }
2526

2627
// 1) server.json — keep version in sync (the official MCP registry schema is strict: no `tools` field,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opengolfapi/mcp-server",
3-
"version": "2.7.0",
3+
"version": "2.8.0",
44
"description": "Golf for AI agents — every US course free, plus open contribution (OpenShot/Moments), portable identity & tournaments. The open data layer for golf.",
55
"mcpName": "io.github.opengolfapi/mcp-server",
66
"type": "module",

server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
33
"name": "io.github.opengolfapi/mcp-server",
44
"description": "Golf for AI agents — every US course free, plus open contribution, identity & tournaments.",
5-
"version": "2.7.0",
5+
"version": "2.8.0",
66
"repository": {
77
"url": "https://github.com/opengolfapi/mcp-server",
88
"source": "github"
@@ -12,7 +12,7 @@
1212
{
1313
"registryType": "npm",
1414
"identifier": "@opengolfapi/mcp-server",
15-
"version": "2.7.0",
15+
"version": "2.8.0",
1616
"transport": {
1717
"type": "stdio"
1818
},

src/index.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,74 @@ server.tool(
605605
}
606606
);
607607

608+
// ── OpenMatch (scoring) — FREE & KEYLESS for GROSS, any of the 15 formats. Net/handicap, money-safe
609+
// settlement, strokes-gained & tournament scale are the gated layer (not here). The game is free. ──────
610+
server.tool(
611+
'list_game_formats',
612+
'List every OpenMatch scoring format (stroke, stableford, match_play, skins, scramble, best_ball, nassau, ctp, longest_drive, greenies, wolf, …) and the free/gated line. GROSS scoring is free + keyless; net/handicap, settlement, strokes-gained & tournament scale are gated. No key needed.',
613+
{},
614+
async () => {
615+
try { return { content: [{ type: 'text' as const, text: JSON.stringify(await apiGet('/api/v1/compute'), null, 2) }] }; }
616+
catch (e) { return { content: [{ type: 'text' as const, text: `Error: ${e instanceof Error ? e.message : String(e)}` }] }; }
617+
}
618+
);
619+
server.tool(
620+
'score_round',
621+
'Score a round in any format — GROSS, FREE, no key. players: [{player_id, holes:{"1":4,...}}] for stroke formats, or entries:[{player_id,hole,value}] for shot formats (ctp/longest_drive/greenies). holes: [{hole,par,stroke_index?}]. Returns standings. NET/handicap is the gated tier (add a compute key); money-safe settlement lives elsewhere.',
622+
{
623+
format: z.string().describe('stroke | stableford | match_play | skins | scramble | best_ball | foursomes | nassau | quota | ryder_cup | ctp | longest_drive | greenies | bingo_bango_bongo | wolf'),
624+
players: z.array(z.any()).optional().describe('stroke formats: [{player_id, holes:{"1":4}}]'),
625+
entries: z.array(z.any()).optional().describe('shot formats: [{player_id, hole, value}]'),
626+
holes: z.array(z.any()).optional().describe('[{hole, par, stroke_index?}]'),
627+
teams: z.array(z.any()).optional(),
628+
rules: z.any().optional().describe('e.g. {amount: 100}. Note: rules.net / playing_handicap escalate to the gated tier.'),
629+
},
630+
async ({ format, players, entries, holes, teams, rules }) => {
631+
const body: any = {}; if (players) body.players = players; if (entries) body.entries = entries; if (holes) body.holes = holes; if (teams) body.teams = teams; if (rules) body.rules = rules;
632+
try {
633+
const res = await customFetch(`${API_BASE}/api/v1/compute/${encodeURIComponent(format)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
634+
const j = await res.json();
635+
return { content: [{ type: 'text' as const, text: JSON.stringify(j, null, 2) }] };
636+
} catch (e) { return { content: [{ type: 'text' as const, text: `Error: ${e instanceof Error ? e.message : String(e)}` }] }; }
637+
}
638+
);
639+
640+
// ── Developer — no OpenGolf ID, no dev key. Sign in (request_sign_in_code → complete_sign_in) to get an
641+
// access_token, then mint/link/list your keys. New keys = free scope (read+contribute+gross). ──────────
642+
server.tool(
643+
'create_dev_key',
644+
'Sign up to build: mint an API key bound to your OpenGolf ID. Requires an OpenGolf ID access_token (from complete_sign_in) + the SAME email you signed in with. No OpenGolf ID, no key. Returns the key ONCE. Free scope = read + contribute + keyless gross scoring; net/settlement/geometry/game need an entitled key ([email protected]).',
645+
{ opengolf_token: z.string().describe('access_token from complete_sign_in'), email: z.string().describe('the email of your OpenGolf ID'), name: z.string().optional(), accept_terms: z.boolean().optional() },
646+
async ({ opengolf_token, email, name, accept_terms }) => {
647+
try {
648+
const res = await customFetch(`${API_BASE}/api/v1/developer/keys`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OpenGolf-Token': opengolf_token }, body: JSON.stringify({ email, name, accept_terms }) });
649+
return { content: [{ type: 'text' as const, text: JSON.stringify(await res.json(), null, 2) }] };
650+
} catch (e) { return { content: [{ type: 'text' as const, text: `Error: ${e instanceof Error ? e.message : String(e)}` }] }; }
651+
}
652+
);
653+
server.tool(
654+
'link_dev_key',
655+
'Confirm your OpenGolf ID on an EXISTING (legacy) key — the simple "add my ID" step. Sign in with the email the key was issued under, then call this with that access_token + the api_key. Verifies + keeps the key and its scopes. (We don\'t revoke legacy keys — they just confirm their ID.)',
656+
{ opengolf_token: z.string().describe('access_token from complete_sign_in'), api_key: z.string().describe('the existing ogapi_ key to link') },
657+
async ({ opengolf_token, api_key }) => {
658+
try {
659+
const res = await customFetch(`${API_BASE}/api/v1/developer/keys/link`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OpenGolf-Token': opengolf_token, 'X-API-Key': api_key }, body: '{}' });
660+
return { content: [{ type: 'text' as const, text: JSON.stringify(await res.json(), null, 2) }] };
661+
} catch (e) { return { content: [{ type: 'text' as const, text: `Error: ${e instanceof Error ? e.message : String(e)}` }] }; }
662+
}
663+
);
664+
server.tool(
665+
'list_dev_keys',
666+
'List the API keys owned by your OpenGolf ID (prefixes only — never the secret). Requires an OpenGolf ID access_token from complete_sign_in.',
667+
{ opengolf_token: z.string().describe('access_token from complete_sign_in') },
668+
async ({ opengolf_token }) => {
669+
try {
670+
const res = await customFetch(`${API_BASE}/api/v1/developer/keys`, { headers: { 'X-OpenGolf-Token': opengolf_token } });
671+
return { content: [{ type: 'text' as const, text: JSON.stringify(await res.json(), null, 2) }] };
672+
} catch (e) { return { content: [{ type: 'text' as const, text: `Error: ${e instanceof Error ? e.message : String(e)}` }] }; }
673+
}
674+
);
675+
608676
// ── OpenGolf ID — profile, beacon, awards, consent (the OPEN player/identity layer; keyed). ──────────
609677
server.tool(
610678
'get_profile',

0 commit comments

Comments
 (0)