feat(mcp): add OAuth-protected MCP server for competitions - #457
feat(mcp): add OAuth-protected MCP server for competitions#457theianjones wants to merge 4 commits into
Conversation
Adds an MCP (Model Context Protocol) server to wodsmith-start that exposes
WODsmith competitions over a streamable HTTP endpoint at /mcp. The worker is
now fronted by @cloudflare/workers-oauth-provider so authenticated clients can
list and read competitions they organize while public events remain available
without auth.
- New OAUTH_KV namespace bound separately from KV_SESSION
- Scopes: events:list, events:read (the demo scope set)
- Public resources/tools (no auth): competition://public/{slug}
- Organizer resources/tools (scoped): competition://organizer/{slug}
- DCR enabled at /oauth/register; consent UI at /oauth/authorize reuses the
existing cookie session and surfaces a scope checklist
- Tools return resource_link blocks for lists and embedded_resource blocks
for individual reads
- Anonymous /mcp requests fall through to a public-only MCP session
https://claude.ai/code/session_01Tye2djkcEcyMYi1wWbqias
WalkthroughThis PR adds a complete Model Context Protocol (MCP) server for WODsmith, exposing competitions via OAuth-protected resources and tools. The implementation includes infrastructure provisioning (Cloudflare KV), scope and grant models, data access queries, MCP server with resource/tool handlers, HTTP transport, OAuth consent flow UI, and integration into the worker entry point. ChangesMCP Server with OAuth Authentication
Sequence Diagram(s)sequenceDiagram
participant User
participant Client as OAuth Client
participant AuthPage as /oauth/authorize
participant OAuthFn as oauth-fns
participant OAuthProvider
participant MCPServer as MCP Server
Client->>AuthPage: redirect with authorize_url + state
AuthPage->>OAuthFn: getAuthorizeConsentInfoFn(authorize_url)
OAuthFn->>OAuthProvider: parseAuthorizeRequest()
OAuthProvider-->>OAuthFn: client + requestedScopes
OAuthFn-->>AuthPage: filtered scopes + supportedScopes
AuthPage->>User: render consent form
User->>AuthPage: select scopes & click Approve
AuthPage->>OAuthFn: completeAuthorizeFn(grantedScopes)
OAuthFn->>OAuthProvider: complete authorization
OAuthProvider-->>OAuthFn: redirectTo URL
OAuthFn-->>AuthPage: redirectTo
AuthPage->>Client: redirect to redirectTo
Client->>MCPServer: POST /mcp with Authorization header
MCPServer->>MCPServer: extract grant props from OAuth context
MCPServer-->>Client: MCP resources/tools based on scope
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
apps/wodsmith-start/src/server-fns/oauth-fns.ts (1)
15-39: ⚡ Quick winPrefer an interface for
OAuthHelpers.Use
interfaceinstead oftypehere for consistency with the project’s TS guideline.As per coding guidelines: `**/*.{ts,tsx}`: Use TypeScript everywhere; prefer interfaces over types.♻️ Proposed refactor
-type OAuthHelpers = { +interface OAuthHelpers { parseAuthRequest(request: Request): Promise<{ responseType: string clientId: string @@ completeAuthorization(options: { @@ }): Promise<{ redirectTo: string }> -} +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/wodsmith-start/src/server-fns/oauth-fns.ts` around lines 15 - 39, Replace the type alias OAuthHelpers with an equivalent interface declaration to follow project TS guidelines: change "type OAuthHelpers = { ... }" to "interface OAuthHelpers { ... }" preserving the exact method signatures for parseAuthRequest, lookupClient, and completeAuthorization (including return shapes and optional fields like codeChallenge/codeChallengeMethod and optional clientName/logoUri/clientUri). Ensure any places that referenced OAuthHelpers still compile (no change to member names/types) and update any exported/imported uses if your linter or build reports need explicit export keywords.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/wodsmith-start/src/mcp/handler.ts`:
- Around line 31-38: The current flow creates server via createMcpServer(props)
then awaits server.connect(transport) outside the try, so server.close() in the
finally won't run if connect throws; modify the control flow so that the
try/finally surrounds both server.connect(transport) and the subsequent
transport.handleRequest(request) (or alternatively catch any error from
server.connect and still await server.close in a finally), ensuring
server.close() is always awaited on the server instance returned by
createMcpServer; reference createMcpServer, server.connect,
transport.handleRequest, and server.close when making the change.
In `@apps/wodsmith-start/src/routes/oauth/authorize.tsx`:
- Around line 123-133: The onDeny handler currently trusts search.redirect_uri
and can open an open-redirect or crash on malformed input; update onDeny to
first validate the redirect URI before setting window.location.href: attempt to
construct new URL(search.redirect_uri) inside a try/catch to handle malformed
URLs, then verify that that URL matches a provider-validated/registered client
redirect (use your app's client registry/validation function or a whitelist
lookup — e.g., call validateRedirectUri(redirect, clientId) or compare against
registeredRedirects for the client) and only then set url.searchParams and
assign window.location.href; if validation fails or throws, fallback to
router.navigate({ to: "/" }) so no external/unverified redirect occurs.
In `@apps/wodsmith-start/src/server-fns/oauth-fns.ts`:
- Around line 140-143: The current safeScopes computation lets any supported
scope in data.grantedScopes be granted even if the client didn't request it;
change it to the intersection of parsed.scope (the client's requested scopes),
data.grantedScopes (what the client allowed), and ALL_MCP_SCOPES. Concretely,
build a requestedSet from parsed.scope, keep the existing supportedSet (new
Set(ALL_MCP_SCOPES)), and set safeScopes = data.grantedScopes.filter(s =>
supportedSet.has(s) && requestedSet.has(s)) before you call
completeAuthorization so only requested+granted+supported scopes are issued.
In `@apps/wodsmith-start/test/mcp/scopes.test.ts`:
- Around line 4-28: Add a single-line reference comment containing the token
"`@lat`" and a short identifier immediately adjacent to the describe("hasScope",
...) spec block so the test section is referenced by the LAT tooling; update the
test file's scopes.test.ts by inserting that single `@lat` comment next to the
describe("hasScope") block (keep exactly one `@lat` comment for this spec
section).
In `@apps/wodsmith-start/test/mcp/server.test.ts`:
- Around line 72-174: Each top-level describe block is missing the required
single-line marker; add exactly one comment line containing "// `@lat`:" adjacent
to each describe for "MCP server — anonymous request", "MCP server —
authenticated with events:list only", and "MCP server — authenticated with
events:read" so every spec section has the marker; place the comment immediately
above (or directly beside) the corresponding describe(...) declaration in
server.test.ts to satisfy the codestyle rule.
---
Nitpick comments:
In `@apps/wodsmith-start/src/server-fns/oauth-fns.ts`:
- Around line 15-39: Replace the type alias OAuthHelpers with an equivalent
interface declaration to follow project TS guidelines: change "type OAuthHelpers
= { ... }" to "interface OAuthHelpers { ... }" preserving the exact method
signatures for parseAuthRequest, lookupClient, and completeAuthorization
(including return shapes and optional fields like
codeChallenge/codeChallengeMethod and optional clientName/logoUri/clientUri).
Ensure any places that referenced OAuthHelpers still compile (no change to
member names/types) and update any exported/imported uses if your linter or
build reports need explicit export keywords.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 458b3342-4b2b-4c06-9750-51d2bb7bba73
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (14)
apps/wodsmith-start/alchemy.run.tsapps/wodsmith-start/package.jsonapps/wodsmith-start/src/mcp/data.tsapps/wodsmith-start/src/mcp/handler.tsapps/wodsmith-start/src/mcp/scopes.tsapps/wodsmith-start/src/mcp/server.tsapps/wodsmith-start/src/routeTree.gen.tsapps/wodsmith-start/src/routes/oauth/authorize.tsxapps/wodsmith-start/src/server-fns/oauth-fns.tsapps/wodsmith-start/src/server.tsapps/wodsmith-start/test/mcp/scopes.test.tsapps/wodsmith-start/test/mcp/server.test.tslat.md/lat.mdlat.md/mcp.md
| describe("hasScope", () => { | ||
| it("returns false when no props are provided (anonymous request)", () => { | ||
| expect(hasScope(undefined, MCP_SCOPES.EVENTS_LIST)).toBe(false) | ||
| expect(hasScope(undefined, MCP_SCOPES.EVENTS_READ)).toBe(false) | ||
| }) | ||
|
|
||
| it("returns false when scope is not in the granted list", () => { | ||
| const props = { userId: "u1", scopes: [MCP_SCOPES.EVENTS_LIST] } | ||
| expect(hasScope(props, MCP_SCOPES.EVENTS_READ)).toBe(false) | ||
| }) | ||
|
|
||
| it("returns true when scope is granted", () => { | ||
| const props = { | ||
| userId: "u1", | ||
| scopes: [MCP_SCOPES.EVENTS_LIST, MCP_SCOPES.EVENTS_READ], | ||
| } | ||
| expect(hasScope(props, MCP_SCOPES.EVENTS_LIST)).toBe(true) | ||
| expect(hasScope(props, MCP_SCOPES.EVENTS_READ)).toBe(true) | ||
| }) | ||
|
|
||
| it("handles missing scopes array defensively", () => { | ||
| const props = { userId: "u1", scopes: undefined as never } | ||
| expect(hasScope(props, MCP_SCOPES.EVENTS_LIST)).toBe(false) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Add a // @lat: marker for this spec section.
This test file has one spec section but no adjacent // @lat: reference comment.
Suggested fix
import { describe, expect, it } from "vitest"
import { hasScope, MCP_SCOPES } from "`@/mcp/scopes`"
+// `@lat`: mcp-scopes-hasScope
describe("hasScope", () => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| describe("hasScope", () => { | |
| it("returns false when no props are provided (anonymous request)", () => { | |
| expect(hasScope(undefined, MCP_SCOPES.EVENTS_LIST)).toBe(false) | |
| expect(hasScope(undefined, MCP_SCOPES.EVENTS_READ)).toBe(false) | |
| }) | |
| it("returns false when scope is not in the granted list", () => { | |
| const props = { userId: "u1", scopes: [MCP_SCOPES.EVENTS_LIST] } | |
| expect(hasScope(props, MCP_SCOPES.EVENTS_READ)).toBe(false) | |
| }) | |
| it("returns true when scope is granted", () => { | |
| const props = { | |
| userId: "u1", | |
| scopes: [MCP_SCOPES.EVENTS_LIST, MCP_SCOPES.EVENTS_READ], | |
| } | |
| expect(hasScope(props, MCP_SCOPES.EVENTS_LIST)).toBe(true) | |
| expect(hasScope(props, MCP_SCOPES.EVENTS_READ)).toBe(true) | |
| }) | |
| it("handles missing scopes array defensively", () => { | |
| const props = { userId: "u1", scopes: undefined as never } | |
| expect(hasScope(props, MCP_SCOPES.EVENTS_LIST)).toBe(false) | |
| }) | |
| }) | |
| // `@lat`: mcp-scopes-hasScope | |
| describe("hasScope", () => { | |
| it("returns false when no props are provided (anonymous request)", () => { | |
| expect(hasScope(undefined, MCP_SCOPES.EVENTS_LIST)).toBe(false) | |
| expect(hasScope(undefined, MCP_SCOPES.EVENTS_READ)).toBe(false) | |
| }) | |
| it("returns false when scope is not in the granted list", () => { | |
| const props = { userId: "u1", scopes: [MCP_SCOPES.EVENTS_LIST] } | |
| expect(hasScope(props, MCP_SCOPES.EVENTS_READ)).toBe(false) | |
| }) | |
| it("returns true when scope is granted", () => { | |
| const props = { | |
| userId: "u1", | |
| scopes: [MCP_SCOPES.EVENTS_LIST, MCP_SCOPES.EVENTS_READ], | |
| } | |
| expect(hasScope(props, MCP_SCOPES.EVENTS_LIST)).toBe(true) | |
| expect(hasScope(props, MCP_SCOPES.EVENTS_READ)).toBe(true) | |
| }) | |
| it("handles missing scopes array defensively", () => { | |
| const props = { userId: "u1", scopes: undefined as never } | |
| expect(hasScope(props, MCP_SCOPES.EVENTS_LIST)).toBe(false) | |
| }) | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/wodsmith-start/test/mcp/scopes.test.ts` around lines 4 - 28, Add a
single-line reference comment containing the token "`@lat`" and a short identifier
immediately adjacent to the describe("hasScope", ...) spec block so the test
section is referenced by the LAT tooling; update the test file's scopes.test.ts
by inserting that single `@lat` comment next to the describe("hasScope") block
(keep exactly one `@lat` comment for this spec section).
| describe("MCP server — anonymous request", () => { | ||
| it("lists only public resources via resources/list", async () => { | ||
| const { client } = await connectClient(undefined) | ||
| const result = await client.listResources() | ||
| const uris = result.resources.map((r) => r.uri) | ||
| expect(uris).toContain(`competition://public/${publicComp.slug}`) | ||
| // organizer template's list callback returns [] when no scope is granted | ||
| expect(uris.some((u) => u.startsWith("competition://organizer/"))).toBe( | ||
| false, | ||
| ) | ||
| }) | ||
|
|
||
| it("reads a public competition", async () => { | ||
| const { client } = await connectClient(undefined) | ||
| const result = await client.readResource({ | ||
| uri: `competition://public/${publicComp.slug}`, | ||
| }) | ||
| expect(result.contents).toHaveLength(1) | ||
| const first = result.contents[0] as { | ||
| mimeType?: string | ||
| text: string | ||
| } | ||
| expect(first.mimeType).toBe("application/json") | ||
| const parsed = JSON.parse(first.text) | ||
| expect(parsed.slug).toBe(publicComp.slug) | ||
| }) | ||
|
|
||
| it("rejects organizer scope when calling list_competitions tool", async () => { | ||
| const { client } = await connectClient(undefined) | ||
| const result = await client.callTool({ | ||
| name: "list_competitions", | ||
| arguments: { scope: "organizer" }, | ||
| }) | ||
| expect(result.isError).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| describe("MCP server — authenticated with events:list only", () => { | ||
| const props: McpGrantProps = { | ||
| userId: "usr_test", | ||
| scopes: [MCP_SCOPES.EVENTS_LIST], | ||
| } | ||
|
|
||
| it("lists organizer competitions including drafts via list_competitions tool", async () => { | ||
| const { client } = await connectClient(props) | ||
| const result = await client.callTool({ | ||
| name: "list_competitions", | ||
| arguments: { scope: "organizer" }, | ||
| }) | ||
| expect(result.isError).toBeFalsy() | ||
| const links = (result.content as Array<{ type: string; uri: string }>).filter( | ||
| (b) => b.type === "resource_link", | ||
| ) | ||
| const uris = links.map((b) => b.uri) | ||
| expect(uris).toContain(`competition://organizer/${draftComp.slug}`) | ||
| expect(listOrganizer).toHaveBeenCalledWith(props.userId) | ||
| }) | ||
|
|
||
| it("rejects get_competition with scope=organizer (events:read missing)", async () => { | ||
| const { client } = await connectClient(props) | ||
| const result = await client.callTool({ | ||
| name: "get_competition", | ||
| arguments: { scope: "organizer", slug: draftComp.slug }, | ||
| }) | ||
| expect(result.isError).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| describe("MCP server — authenticated with events:read", () => { | ||
| const props: McpGrantProps = { | ||
| userId: "usr_test", | ||
| scopes: [MCP_SCOPES.EVENTS_LIST, MCP_SCOPES.EVENTS_READ], | ||
| } | ||
|
|
||
| it("returns an embedded_resource block for a draft competition the user organizes", async () => { | ||
| const { client } = await connectClient(props) | ||
| const result = await client.callTool({ | ||
| name: "get_competition", | ||
| arguments: { scope: "organizer", slug: draftComp.slug }, | ||
| }) | ||
| expect(result.isError).toBeFalsy() | ||
| const block = (result.content as Array<{ | ||
| type: string | ||
| resource: { uri: string; text: string } | ||
| }>)[0]! | ||
| expect(block.type).toBe("resource") | ||
| expect(block.resource.uri).toBe( | ||
| `competition://organizer/${draftComp.slug}`, | ||
| ) | ||
| const parsed = JSON.parse(block.resource.text) | ||
| expect(parsed.slug).toBe(draftComp.slug) | ||
| expect(parsed.status).toBe("draft") | ||
| }) | ||
|
|
||
| it("returns 404-style error when slug doesn't exist", async () => { | ||
| const { client } = await connectClient(props) | ||
| const result = await client.callTool({ | ||
| name: "get_competition", | ||
| arguments: { scope: "organizer", slug: "nope" }, | ||
| }) | ||
| expect(result.isError).toBe(true) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Add one // @lat: comment per describe section.
This file defines three spec sections but none has the required adjacent // @lat: marker.
Suggested fix
+// `@lat`: mcp-server-anonymous
describe("MCP server — anonymous request", () => {
@@
+// `@lat`: mcp-server-events-list-only
describe("MCP server — authenticated with events:list only", () => {
@@
+// `@lat`: mcp-server-events-read
describe("MCP server — authenticated with events:read", () => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/wodsmith-start/test/mcp/server.test.ts` around lines 72 - 174, Each
top-level describe block is missing the required single-line marker; add exactly
one comment line containing "// `@lat`:" adjacent to each describe for "MCP server
— anonymous request", "MCP server — authenticated with events:list only", and
"MCP server — authenticated with events:read" so every spec section has the
marker; place the comment immediately above (or directly beside) the
corresponding describe(...) declaration in server.test.ts to satisfy the
codestyle rule.
There was a problem hiding this comment.
4 issues found across 15 files
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/wodsmith-start/src/server-fns/oauth-fns.ts (1)
103-133: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winAdd a
//@lat`` reference on the consent completion path.This authorization block changed, but it still has no repo-required traceability marker back to
lat.md. Please add a//@lat: [[...]]comment abovecompleteAuthorizeFnhere, and ideally mirror that abovegetAuthorizeConsentInfoFnas well.As per coding guidelines:
Add code reference comments (\//@lat: [[section-id]]` for JS/TS/Rust/Go/C or `#@lat: [[section-id]]` for Python) to tie source code to design concepts and test specifications in lat.md.`🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/wodsmith-start/src/server-fns/oauth-fns.ts` around lines 103 - 133, Add the required repo traceability marker by inserting a `// `@lat`: [[section-id]]` comment immediately above the exported completeAuthorizeFn declaration (export const completeAuthorizeFn = ...) and also add the same-style `// `@lat`: [[section-id]]` comment above the getAuthorizeConsentInfoFn declaration so both functions (completeAuthorizeFn and getAuthorizeConsentInfoFn) are linked to the appropriate lat.md section; choose the correct section-id to match the design/test spec and place the comments on the line directly above each function declaration.
🧹 Nitpick comments (2)
apps/wodsmith-start/src/mcp/data.ts (2)
150-153: ⚡ Quick winSwitch this exported API to a named parameter object.
Both inputs are
string, so this signature is easy to call in the wrong order. Using a named object keeps the contract self-describing and matches the repo rule.Proposed change
+interface GetOrganizerCompetitionBySlugArgs { + userId: string + slug: string +} + export async function getOrganizerCompetitionBySlug( - userId: string, - slug: string, + { userId, slug }: GetOrganizerCompetitionBySlugArgs, ): Promise<McpCompetitionSummary | null> {As per coding guidelines "
**/*.{ts,tsx}: Use named object parameters for functions with more than one parameter`."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/wodsmith-start/src/mcp/data.ts` around lines 150 - 153, The exported function getOrganizerCompetitionBySlug currently takes two positional string params (userId, slug) which can be swapped; change its signature to accept a single named parameter object (e.g. { userId: string; slug: string }) and update the return type Promise<McpCompetitionSummary | null> unchanged; update all internal references and external callers to call getOrganizerCompetitionBySlug({ userId, slug }) and adjust any tests or usages accordingly, ensuring any places that destructure its args (or pass positional args) are updated to the new object form.
25-43: ⚡ Quick winAdd the required
//@lat`` annotations for this new MCP surface.The new DTO, exported query helpers, and organizer-permission helper are all new design entry points, but none of them are linked back to the corresponding LAT sections yet.
As per coding guidelines "
**/*.{ts,tsx,js,jsx,py,rs,go,c,h}: Add code reference comments (//@lat: [[section-id]]for JS/TS/Rust/Go/C or#@lat: [[section-id]]for Python) to tie source code to design concepts and test specifications in lat.md."Also applies to: 69-70, 92-94, 119-121, 150-153, 174-176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/wodsmith-start/src/mcp/data.ts` around lines 25 - 43, Add the required LAT reference comments for this new MCP surface by placing // `@lat`: [[section-id]] annotations adjacent to the new exported DTO and helpers: add a // `@lat`: [[section-id]] above the McpCompetitionSummary interface declaration, and add corresponding // `@lat`: [[section-id]] comments above each exported query helper and the organizer-permission helper (the exported functions/classes related to querying competitions and checking organizer permissions) so each new design entry point is linked to its LAT section; use the exact comment format // `@lat`: [[section-id]] for JS/TS and ensure the correct section IDs from lat.md are used for the five indicated locations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/wodsmith-start/src/lib/oauth-context.ts`:
- Around line 15-23: Change runWithOAuthHelpers to accept a single
named-parameter object and add the required lat reference comment above it: add
a comment line like // `@lat`: [[your-section-id]] before the function, then
change the signature from runWithOAuthHelpers<T>(helpers: OAuthHelpers |
undefined, fn: () => T): T to runWithOAuthHelpers<T>({ helpers, fn }: {
helpers?: OAuthHelpers; fn: () => T }): T, keep the internal logic using the
existing AsyncLocalStorage instance (storage) and preserve the early-return when
helpers is undefined, and update any call sites to pass an object ({ helpers, fn
}) accordingly so call semantics remain equivalent.
- Around line 12-15: The module imports AsyncLocalStorage and creates storage
(const storage = new AsyncLocalStorage<OAuthHelpers>()), so update the project
compatibility flags to include nodejs_als (add "nodejs_als" to
compatibility_flags in your wrangler config) to ensure ALS is available at
runtime; add the required comment at the top of oauth-context.ts exactly as //
`@lat`: [[section-id]]; and refactor runWithOAuthHelpers to accept a single named
parameter object (e.g., runWithOAuthHelpers({ helpers, fn })) instead of two
positional args, update callers to the new signature, and keep existing helpers
accessor functions (e.g., getOAuthHelpers) unchanged.
In `@apps/wodsmith-start/src/server.ts`:
- Around line 259-267: Add a // `@lat`: [[section-id]] comment above the OAuth
wiring block so this entrypoint is tied to the MCP/OAuth design doc;
specifically, insert the // `@lat`: [[section-id]] comment immediately before the
code that captures OAUTH_PROVIDER (the helpers assignment) and the
runWithOAuthHelpers(...) call that wraps fetchWithLogging, so the
runWithOAuthHelpers, helpers, and fetchWithLogging wiring is referenced from
lat.md.
In `@apps/wodsmith-start/vite.config.ts`:
- Line 20: The devtools config line (devtools({ eventBusConfig: { port: 41000 }
}),) is missing the required trace tag; add a matching single-line comment //
`@lat`: [[section-id]] adjacent to that config entry so the file is linked to the
design/test spec in lat.md (place the comment on the same line or immediately
above the devtools(...) call and replace [[section-id]] with the correct section
id).
---
Outside diff comments:
In `@apps/wodsmith-start/src/server-fns/oauth-fns.ts`:
- Around line 103-133: Add the required repo traceability marker by inserting a
`// `@lat`: [[section-id]]` comment immediately above the exported
completeAuthorizeFn declaration (export const completeAuthorizeFn = ...) and
also add the same-style `// `@lat`: [[section-id]]` comment above the
getAuthorizeConsentInfoFn declaration so both functions (completeAuthorizeFn and
getAuthorizeConsentInfoFn) are linked to the appropriate lat.md section; choose
the correct section-id to match the design/test spec and place the comments on
the line directly above each function declaration.
---
Nitpick comments:
In `@apps/wodsmith-start/src/mcp/data.ts`:
- Around line 150-153: The exported function getOrganizerCompetitionBySlug
currently takes two positional string params (userId, slug) which can be
swapped; change its signature to accept a single named parameter object (e.g. {
userId: string; slug: string }) and update the return type
Promise<McpCompetitionSummary | null> unchanged; update all internal references
and external callers to call getOrganizerCompetitionBySlug({ userId, slug }) and
adjust any tests or usages accordingly, ensuring any places that destructure its
args (or pass positional args) are updated to the new object form.
- Around line 25-43: Add the required LAT reference comments for this new MCP
surface by placing // `@lat`: [[section-id]] annotations adjacent to the new
exported DTO and helpers: add a // `@lat`: [[section-id]] above the
McpCompetitionSummary interface declaration, and add corresponding // `@lat`:
[[section-id]] comments above each exported query helper and the
organizer-permission helper (the exported functions/classes related to querying
competitions and checking organizer permissions) so each new design entry point
is linked to its LAT section; use the exact comment format // `@lat`:
[[section-id]] for JS/TS and ensure the correct section IDs from lat.md are used
for the five indicated locations.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4888b3a9-c5c6-48d0-823d-2b193db41726
📒 Files selected for processing (7)
apps/wodsmith-start/src/lib/oauth-context.tsapps/wodsmith-start/src/mcp/data.tsapps/wodsmith-start/src/server-fns/oauth-fns.tsapps/wodsmith-start/src/server.tsapps/wodsmith-start/vite.config.tslat.md/auth.mdlat.md/mcp.md
✅ Files skipped from review due to trivial changes (2)
- lat.md/auth.md
- lat.md/mcp.md
| import { AsyncLocalStorage } from "node:async_hooks" | ||
| import type { OAuthHelpers } from "@cloudflare/workers-oauth-provider" | ||
|
|
||
| const storage = new AsyncLocalStorage<OAuthHelpers>() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -i 'alchemy\.run\.ts|wrangler\.(jsonc|toml|js|ts)|worker-configuration\.d\.ts' . | while read -r file; do
echo "== $file =="
rg -n -C2 'nodejs_compat|nodejs_als|compatibility_date|compatibilityDate' "$file" || true
doneRepository: wodsmith/thewodapp
Length of output: 1886
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "nodejs_als" apps --glob 'wrangler.*' || true
rg -n "AsyncLocalStorage|node:async_hooks|async_hooks" apps/wodsmith-start/src -S || trueRepository: wodsmith/thewodapp
Length of output: 1514
🌐 Web query:
Cloudflare Workers nodejs_als flag AsyncLocalStorage node:async_hooks requirements
💡 Result:
In Cloudflare Workers, to use Node.js AsyncLocalStorage (from node:async_hooks) you typically need the Wrangler compatibility flag nodejs_als, and you use the Node-style import form import { AsyncLocalStorage } from "node:async_hooks". 1) When you need nodejs_als - Cloudflare Workers provides an AsyncLocalStorage implementation as a subset of Node’s API; it’s available when you enable the nodejs_als compatibility flag in your Wrangler config [1][2]. - The Node.js compatibility docs explicitly say: “If you need to enable only the Node.js AsyncLocalStorage API, you can enable the nodejs_als compatibility flag” [1]. 2) How to enable it (Wrangler config) - Add nodejs_als under compatibility_flags in either wrangler.jsonc or wrangler.toml [1]. Example (wrangler.toml): compatibility_flags = [ "nodejs_als" ] [1] (If you’re seeing missing/unsupported Node.js imports for ALS, this flag is what enables it.) 3) Import / runtime behavior requirements (what Workers does and does NOT implement) - You import it using the Node specifier form, e.g. import { AsyncLocalStorage } from "node:async_hooks" [2]. - Workers provides coherent async context tracking across async operations, but it is intentionally not a full Node async_hooks implementation [2]. - Specifically, Workers’ AsyncLocalStorage intentionally omits asyncLocalStorage.enterWith() and asyncLocalStorage.disable() [2]. - Workers also does not implement the ability to create an AsyncResource with an explicitly identified trigger context (so new AsyncResources are always bound to the context in which they were created) [2]. - Thenables are not fully supported for AsyncLocalStorage; for thenables you’re instructed to use AsyncLocalStorage.snapshot() to capture the current context [2]. 4) Practical takeaway - If your code only needs Node AsyncLocalStorage, prefer enabling only nodejs_als (not the full Node compatibility layer) via compatibility_flags = ["nodejs_als"] [1]. - Ensure your code avoids ALS APIs that Workers explicitly does not implement (notably enterWith() and disable()), and prefer run()/snapshot()/bind() patterns that Workers documents [2]. Sources: [1] https://developers.cloudflare.com/workers/runtime-apis/nodejs/ [2] https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/
Citations:
- 1: https://developers.cloudflare.com/workers/runtime-apis/nodejs/
- 2: https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,120p' apps/wodsmith-start/src/lib/oauth-context.ts
rg -n "`@lat`:" apps/wodsmith-start/src/lib/oauth-context.ts || true
rg -n "runWithOAuthHelpers" apps/wodsmith-start/src/lib/oauth-context.tsRepository: wodsmith/thewodapp
Length of output: 1028
🌐 Web query:
Does Cloudflare Wrangler compatibility flag nodejs_compatimplicitly enablenodejs_als(AsyncLocalStorage) when usingimport { AsyncLocalStorage } from "node:async_hooks"?
💡 Result:
No. Cloudflare’s nodejs_compat compatibility flag does not implicitly enable nodejs_als. Cloudflare documents that nodejs_compat is used “to enable built-in Node.js APIs and add polyfills” when paired with an appropriate compatibility_date [1][2]. Separately, it states that “If you need to enable only the Node.js AsyncLocalStorage API, you can enable the nodejs_als compatibility flag” (shown as using compatibility_flags: ["nodejs_als"]) [1][2]. This separation indicates nodejs_als is an independent flag rather than being implied by nodejs_compat. Additionally, the Cloudflare docs for the AsyncLocalStorage API show the intended import form (import { AsyncLocalStorage } from "node:async_hooks") but do not state that nodejs_compat turns on nodejs_als; rather, it describes enabling Node.js APIs via nodejs_compat and enabling only AsyncLocalStorage via nodejs_als [3][1]. Confidence: high (based on Cloudflare’s own compatibility-flag documentation explicitly treating nodejs_als as “only AsyncLocalStorage”).
Citations:
- 1: https://developers.cloudflare.com/workers/runtime-apis/nodejs/
- 2: https://developers.cloudflare.com/workers/configuration/compatibility-flags/
- 3: https://37731a98.preview.developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/
Enable nodejs_als (and fix guideline violations) for AsyncLocalStorage in oauth-context
apps/wodsmith-start/wrangler.jsoncincludesnodejs_compatbut notnodejs_als, whileapps/wodsmith-start/src/lib/oauth-context.tsimportsAsyncLocalStoragefromnode:async_hooks(also used in a few otherapps/wodsmith-startfiles), so the context storage may not be available at runtime—addnodejs_alstocompatibility_flags.- Add the required
//@lat: [[section-id]]comment toapps/wodsmith-start/src/lib/oauth-context.ts. - Refactor
runWithOAuthHelpersto use a named object parameter instead of two positional parameters.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/wodsmith-start/src/lib/oauth-context.ts` around lines 12 - 15, The
module imports AsyncLocalStorage and creates storage (const storage = new
AsyncLocalStorage<OAuthHelpers>()), so update the project compatibility flags to
include nodejs_als (add "nodejs_als" to compatibility_flags in your wrangler
config) to ensure ALS is available at runtime; add the required comment at the
top of oauth-context.ts exactly as // `@lat`: [[section-id]]; and refactor
runWithOAuthHelpers to accept a single named parameter object (e.g.,
runWithOAuthHelpers({ helpers, fn })) instead of two positional args, update
callers to the new signature, and keep existing helpers accessor functions
(e.g., getOAuthHelpers) unchanged.
| const storage = new AsyncLocalStorage<OAuthHelpers>() | ||
|
|
||
| export function runWithOAuthHelpers<T>( | ||
| helpers: OAuthHelpers | undefined, | ||
| fn: () => T, | ||
| ): T { | ||
| if (!helpers) return fn() | ||
| return storage.run(helpers, fn) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Align this helper with the repo’s API-shape and traceability rules.
runWithOAuthHelpers is a new exported helper, but it still uses positional params and has no // @lat: [[...]] link. Please switch it to a named-parameter object and add the lat reference before more call sites depend on this signature.
♻️ Example shape
+// `@lat`: [[oauth-consent-flow]]
export function runWithOAuthHelpers<T>(
- helpers: OAuthHelpers | undefined,
- fn: () => T,
+ {
+ helpers,
+ fn,
+ }: {
+ helpers: OAuthHelpers | undefined
+ fn: () => T
+ },
): T {
if (!helpers) return fn()
return storage.run(helpers, fn)
}As per coding guidelines: Add code reference comments (\// @lat: [[section-id]]` for JS/TS/Rust/Go/C or `# @lat: [[section-id]]` for Python) to tie source code to design concepts and test specifications in lat.md.andUse named object parameters for functions with more than one parameter`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/wodsmith-start/src/lib/oauth-context.ts` around lines 15 - 23, Change
runWithOAuthHelpers to accept a single named-parameter object and add the
required lat reference comment above it: add a comment line like // `@lat`:
[[your-section-id]] before the function, then change the signature from
runWithOAuthHelpers<T>(helpers: OAuthHelpers | undefined, fn: () => T): T to
runWithOAuthHelpers<T>({ helpers, fn }: { helpers?: OAuthHelpers; fn: () => T
}): T, keep the internal logic using the existing AsyncLocalStorage instance
(storage) and preserve the early-return when helpers is undefined, and update
any call sites to pass an object ({ helpers, fn }) accordingly so call semantics
remain equivalent.
| // The OAuth provider mutates `env` to add `OAUTH_PROVIDER` before calling | ||
| // us — capture it here so server functions can read it via AsyncLocalStorage | ||
| // (it's not visible through `cloudflare:workers`'s env import). | ||
| const helpers = (env as { OAUTH_PROVIDER?: unknown }).OAUTH_PROVIDER as | ||
| | import("@cloudflare/workers-oauth-provider").OAuthHelpers | ||
| | undefined | ||
| return runWithOAuthHelpers(helpers, () => | ||
| fetchWithLogging(request, env as Env, ctx), | ||
| ) |
There was a problem hiding this comment.
Add a // @lat: reference for the OAuth/MCP entrypoint wiring.
This worker integration changes auth and routing behavior, so it should be tied back to the matching MCP/OAuth section in lat.md. As per coding guidelines, **/*.{ts,tsx,js,jsx,py,rs,go,c,h}: Add code reference comments (// @lat: [[section-id]] for JS/TS/Rust/Go/C or # @lat: [[section-id]] for Python) to tie source code to design concepts and test specifications in lat.md.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/wodsmith-start/src/server.ts` around lines 259 - 267, Add a // `@lat`:
[[section-id]] comment above the OAuth wiring block so this entrypoint is tied
to the MCP/OAuth design doc; specifically, insert the // `@lat`: [[section-id]]
comment immediately before the code that captures OAUTH_PROVIDER (the helpers
assignment) and the runWithOAuthHelpers(...) call that wraps fetchWithLogging,
so the runWithOAuthHelpers, helpers, and fetchWithLogging wiring is referenced
from lat.md.
| }, | ||
| }), | ||
| devtools(), | ||
| devtools({ eventBusConfig: { port: 41000 } }), |
There was a problem hiding this comment.
Add the missing // @lat: trace tag.
This config change should carry the matching // @lat: [[section-id]] reference so it stays linked to the relevant design/test spec in lat.md. As per coding guidelines, **/*.{ts,tsx,js,jsx,py,rs,go,c,h}: Add code reference comments (// @lat: [[section-id]] for JS/TS/Rust/Go/C or # @lat: [[section-id]] for Python) to tie source code to design concepts and test specifications in lat.md.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/wodsmith-start/vite.config.ts` at line 20, The devtools config line
(devtools({ eventBusConfig: { port: 41000 } }),) is missing the required trace
tag; add a matching single-line comment // `@lat`: [[section-id]] adjacent to that
config entry so the file is linked to the design/test spec in lat.md (place the
comment on the same line or immediately above the devtools(...) call and replace
[[section-id]] with the correct section id).
…ition-server-t6MyC # Conflicts: # apps/wodsmith-start/src/server.ts # apps/wodsmith-start/vite.config.ts # lat.md/auth.md # lat.md/lat.md # pnpm-lock.yaml
Adds an MCP (Model Context Protocol) server to wodsmith-start that exposes WODsmith competitions over a streamable HTTP endpoint at /mcp. The worker is now fronted by @cloudflare/workers-oauth-provider so authenticated clients can list and read competitions they organize while public events remain available without auth.
https://claude.ai/code/session_01Tye2djkcEcyMYi1wWbqias
Summary by cubic
Adds a streamable HTTP MCP server at
/mcpfor WODsmith competitions. Anonymous users can browse public events; OAuth tokens unlock organizer lists and reads with strict scope checks.New Features
competition://public/{slug}andcompetition://organizer/{slug}; toolslist_competitionsandget_competitionreturnresource_link/embedded_resource.@cloudflare/workers-oauth-providerfronts the worker; grants only supported+requested scopes and stores{ userId, scopes }; organizer list requiresevents:list, reads requireevents:read; unauthenticated/mcpserves public-only./oauth/authorize(reuses session, redirects to sign-in if needed, disables non-requested scopes, ignores unknown scopes; denial returnserror=access_denied); DCR at/oauth/register; token at/oauth/token; added tests and docs.Migration
OAUTH_KV.Written for commit a67a646. Summary will update on new commits.
Summary by CodeRabbit
New Features
Documentation