Skip to content

feat((versioning): Implemented versioned route apps - #63

Merged
gabrypavanello merged 11 commits into
mainfrom
versioning
Jan 5, 2026
Merged

feat((versioning): Implemented versioned route apps#63
gabrypavanello merged 11 commits into
mainfrom
versioning

Conversation

@gabrypavanello

@gabrypavanello gabrypavanello commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Note

API versioning (core)

  • createApp now supports multi-version apps via versions, exposing per-version endpoints like /v1/mcp, /v2/mcp
  • New app.getVersion(key) and app.getVersions() for programmatic access
  • Per-version tools, UI, config overrides, plugins, and middleware; global config/plugins are merged
  • Config validation expanded (version key pattern, serverRoute checks); versioned server uses a shared Express app with shared GET /health
  • Server updated to accept a version route and add global endpoints only for single-version apps; OAuth middleware accepts a lazy JWKS client getter

Docs and examples

  • README and packages/core/README.md document versioning usage and patterns
  • examples/minimal converted to a versioned app with v1/v2 greet tools and version-specific React widgets

Tests

  • New unit tests cover version keys, config merging, route isolation, middleware per version, and backward compatibility

Written by Cursor Bugbot for commit 0233cd6. Configure here.

… tools

- Updated README to reflect versioning support and added API endpoint details.
- Refactored index.ts to define separate greet tools for v1 (name only) and v2 (name + surname).
- Removed deprecated GreetingWidget component and created version-specific UI components.
- Enhanced styles.css to support version badges and improved UI layout.
- Updated core to support multi-version app configurations and validation.
- Enhanced README with clearer API endpoint descriptions and input/output specifications.
- Improved formatting in GreetingWidget components for better readability.
- Refactored createApp.ts and server/index.ts for consistent code style and lazy initialization of JWKS client.
- Updated OAuth middleware to support JWKS client as a getter function.
- Introduced a new configuration option for the app to specify the protocol as "openai".
- Updated createApp.ts to manage a shared HTTP server for multi-version applications, enhancing server instance accessibility.
- Improved comments for clarity regarding server instance handling and debug logger configuration.
- Added a new section on API versioning, explaining how to expose multiple versions from a single app.
- Included code examples demonstrating version-specific tools, configuration overrides, and middleware.
- Updated existing examples to reflect API versioning capabilities.
…ormatting

- Added blank lines for better readability in README and core documentation.
- Reformatted tool definitions in the core README for consistency.
- Ensured consistent code style across examples in the documentation.
@coderabbitai

coderabbitai Bot commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • API Versioning: serve multiple API versions from one app with per-version endpoints, tools, configs, merged overrides, programmatic discovery (getVersions/getVersion), and backward compatibility.
  • Documentation

    • Expanded READMEs and examples with versioned setup, endpoints, testing commands, integration guidance, and migration notes.
  • UI

    • Per-version UI components, version badges, updated example widgets and styles.
  • Tests

    • New unit tests covering multi-version behavior, routing, and compatibility.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds API versioning: new multi-version config types and AppConfigInput union, branching createApp into single- and multi-version flows, per-version routing with lazy JWKS init, server changes for versioned endpoints, App.getVersion/getVersions methods, updated examples/docs, UI changes for minimal example, and unit tests.

Changes

Cohort / File(s) Summary
Core config & public types
packages/core/src/types/config.ts, packages/core/src/index.ts
Add multi-version types (VersionConfig, VersionsConfig, VersionSpecificConfig, DeepPartialWithNull, AppConfigInput) and re-export new types.
App orchestration & API
packages/core/src/createApp.ts, packages/core/src/types/tools.ts
Refactor createApp to handle single- or multi-version input; add createSingleVersionApp / createMultiVersionApp, validation/merge helpers, and public getVersion / getVersions methods.
Server & OAuth middleware
packages/core/src/server/index.ts, packages/core/src/server/oauth/middleware.ts
Server accepts optional versionRoute; JWKS may be a lazy getter; OAuth middleware resolves lazy JWKS at runtime; health/404 mounting adjusted for versioned servers.
Tests
packages/core/tests/unit/versioning.test.ts
New unit tests covering multi-version creation, validation, routing, middleware, config/plugin merging, single-version compatibility, and serverless handling.
Docs & READMEs
README.md, packages/core/README.md, examples/minimal/README.md
Add API Versioning documentation, usage and programmatic examples, update Features/Examples lists and local example instructions.
Minimal example runtime
examples/minimal/src/index.ts
Replace single-tool demo with versioned app (v1/v2): per-version tool definitions, schemas, UI mappings, versions map, and version-aware startup/routes.
Minimal example UI & styles
examples/minimal/src/ui/GreetingWidgetV1.tsx, examples/minimal/src/ui/GreetingWidgetV2.tsx, examples/minimal/src/ui/styles.css
Rename V1 widget, add V2 widget (modal with name+surname), update generics to per-version AppClientTools, add CSS for version badges, inputs, and full-name display.
Example docs & structure
examples/minimal/README.md, examples/minimal/*
Update example docs, testing guidance (curl), Claude Desktop config examples, and project structure notes for versioned app.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Client
    participant Express as Shared Express App
    participant Router as Version Router
    participant V1 as V1 App Instance
    participant V2 as V2 App Instance
    participant JWKS as JWKS Client (lazy)
    participant Tools as Per-version Tools

    Note over Client,Express: Multi-version request dispatch

    Client->>+Express: POST /v1/mcp
    Express->>+Router: route /v1
    Router->>+V1: forward request
    alt OAuth required
        V1->>+JWKS: resolve/init JWKS (lazy, per-version)
        JWKS-->>-V1: jwks client
        V1->>V1: verify JWT via middleware
    end
    V1->>+Tools: invoke v1 tool
    Tools-->>-V1: return result
    V1-->>-Router: respond
    Router-->>-Express: response
    Express-->>-Client: 200 OK

    Client->>+Express: POST /v2/mcp
    Express->>+Router: route /v2
    Router->>+V2: forward request
    alt OAuth required
        V2->>+JWKS: resolve/init JWKS (lazy, per-version)
        JWKS-->>-V2: jwks client
        V2->>V2: verify JWT via middleware
    end
    V2->>+Tools: invoke v2 tool
    Tools-->>-V2: return result
    V2-->>-Router: respond
    Router-->>-Express: response
    Express-->>-Client: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature: implementation of versioned route apps, which aligns with the core objective of adding multi-version support to createApp.
Description check ✅ Passed The description provides detailed, structured information about versioning capabilities (createApp multi-version support, getVersion/getVersions methods, per-version config/tools/UI/middleware), documentation updates, and test additions, directly relating to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28512d1 and f86edf3.

📒 Files selected for processing (2)
  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread packages/core/tests/unit/versioning.test.ts Fixed
@gabrypavanello
gabrypavanello marked this pull request as ready for review January 5, 2026 14:16
@claude

claude Bot commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Code Review: API Versioning Feature

This PR introduces a comprehensive API versioning system. Overall, this is a well-implemented feature with excellent test coverage and documentation.

✅ Strengths

Architecture & Design

  • Clean separation between single-version and multi-version code paths
  • Efficient shared Express app across versions
  • Smart OAuth client reuse using config hashing (createApp.ts:719-723)

Test Coverage

  • Comprehensive 710-line test suite covering multi-version creation, config merging, route isolation, and backward compatibility
  • Integration tests with real HTTP server and MCP SDK client

Documentation

  • Excellent README updates with clear examples
  • Updated minimal example demonstrating versioning

🔍 Critical Issues

1. OAuth Config Hashing Not Deterministic (createApp.ts:720)
JSON.stringify(normalizedVersionConfig.config.oauth) is not deterministic for object key ordering. Two identical configs with different key orders will create separate JWKS clients.

Recommendation: Use sorted keys before stringifying or use a proper hash function.

2. Missing JWKS Client Cleanup (createApp.ts:665-918)
Multi-version apps store JWKS clients in a Map but never clean them up. This could prevent process exit and cause memory leaks.

Recommendation: Add cleanup in stop() method or new shutdown() method.

⚠️ High Priority Issues

3. Dead Code in Version Route Validation (createApp.ts:288-293)
The check if (versionRoute === '/health') can never trigger since version keys must match /^v\d+$/.

4. Missing Error Handling in once() (createApp.ts:1022-1063)
The multi-version once() implementation needs try-catch around handler invocation to ensure cleanup happens even on error.

5. Debug Logger Limitation (createApp.ts:704-707)
Version-specific debug configs are ignored (global singleton). Document this clearly or support per-version loggers.

📝 Medium Priority Issues

6. Type Assertion Bypass (createApp.ts:960-970)
return firstVersion as unknown as McpServer bypasses TypeScript safety. Consider refactoring types.

7. Unused Parameter (createApp.ts:979, server/index.ts:339)
The env parameter in handleRequest() is never used. Either implement or remove.

8. Magic Numbers (createApp.ts:764-768)
OAuth config values (600000, 10, 5000) should be extracted as constants.

9. Confusing Naming (types/config.ts:248-278)
VersionConfig has optional config property. Consider renaming to overrides for clarity.

🔒 Security Analysis

✅ No critical security issues found

  • Proper OAuth JWKS discovery and token validation
  • Input validation via Zod schemas
  • Proper route isolation between versions
  • Configurable CORS

Minor note: OAuth error messages at createApp.ts:784 might leak sensitive server URLs in production.

📊 Performance Notes

Good characteristics:

  • JWKS client reuse optimization
  • Lazy OAuth initialization
  • Content hashing for UI cache busting

Concerns:

  • Synchronous fs.readFileSync() at server/index.ts:978 blocks event loop
  • JSON.stringify in OAuth config hashing hot path

🎯 Recommendations

Must Fix:

  1. Make OAuth config hashing deterministic
  2. Add JWKS client cleanup mechanism

Should Fix:
3. Remove dead validation code or expand it
4. Add error handling in once()
5. Document debug logger limitation

✅ Conclusion

This is a high-quality PR with excellent test coverage (710 lines) and documentation. The architecture is sound and maintains backward compatibility. Address the two critical issues before merging.

Recommendation: Approve with requested changes

Great work! 🎉

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Fix all issues with AI Agents 🤖
In @examples/minimal/src/ui/GreetingWidgetV2.tsx:
- Line 92: In GreetingWidgetV2, the name input's onKeyDown currently checks
`e.key === "Enter" && surname && handleGreet()` which blocks Enter when surname
is empty; remove the `surname` guard so pressing Enter in the name field invokes
handleGreet regardless of surname (e.g. change the handler to `e.key === "Enter"
&& handleGreet()`), and ensure any required validation stays inside handleGreet
so optional surname doesn't prevent submission.

In @packages/core/src/createApp.ts:
- Around line 944-957: The current code only assigns sharedHttpServer to the
first ServerInstance (firstVersion.httpServer) causing getServer().httpServer to
be undefined on other versions; update the resolve callback to iterate over
versionServerInstances.values() and assign sharedHttpServer to each
ServerInstance.httpServer so every version instance gets the same HTTP server
(keep using the existing sharedHttpServer, ServerInstance shape, and preserve
resolve/reject behavior).
♻️ Duplicate comments (1)
packages/core/tests/unit/versioning.test.ts (1)

8-8: Remove unused imports: beforeEach and AddressInfo.

beforeEach is imported but never used. Additionally, AddressInfo (line 11) is imported but not referenced anywhere in the file. As per coding guidelines, unused imports should be removed.

Proposed fix
-import { describe, it, expect, beforeEach, afterEach } from "vitest";
+import { describe, it, expect, afterEach } from "vitest";
 import { z } from "zod";
 import { createApp, type AppConfigInput, type VersionsConfig } from "../../src/index";
-import type { AddressInfo } from "node:net";
🧹 Nitpick comments (7)
packages/core/tests/unit/versioning.test.ts (2)

15-31: Unused servers array — cleanup logic never populates it.

The servers array is declared and cleaned up in afterEach, but tests never push servers to it. Instead, each test manually closes app.getServer().httpServer. Either remove the unused tracking array or refactor tests to use it consistently.

Option A: Remove unused code
-// Track servers for cleanup
-const servers: Array<{ close: () => void }> = [];
-
-afterEach(async () => {
-  // Close all servers after each test
-  for (const server of servers) {
-    await new Promise<void>((resolve) => {
-      try {
-        server.close();
-        resolve();
-      } catch {
-        resolve();
-      }
-    });
-  }
-  servers.length = 0;
-});
Option B: Use the array for consistent cleanup

Refactor tests to push servers to the array and rely on afterEach for cleanup, removing the manual httpServer.close() calls in each test.


340-390: Hardcoded ports may cause flaky tests in parallel execution.

Tests use fixed ports (3100–3105). If tests run in parallel or another process occupies these ports, tests will fail. Consider using port 0 to let the OS assign an available port, then retrieve it from the server's address.

Example approach
await app.start({ port: 0 });
const httpServer = app.getServer().httpServer;
const address = httpServer?.address();
const port = typeof address === 'object' && address ? address.port : 0;
// Use `port` for client connections
packages/core/src/types/config.ts (1)

313-340: Consider branded/template literal types for stricter version key validation.

The versions field uses Record<string, VersionConfig<T>>, relying on runtime validation for the v\d+ pattern. For stronger compile-time safety, you could use a template literal type:

type VersionKey = `v${number}`;
versions: Record<VersionKey, VersionConfig<T>>;

This is optional since runtime validation catches invalid keys.

packages/core/src/createApp.ts (4)

336-364: Clarify shallow merge behavior for nested config objects.

The merge logic uses nullish coalescing (??) for nested objects like oauth, cors, etc., which means version-specific config entirely replaces global config rather than deep-merging. This is likely intentional but worth documenting explicitly in the JSDoc comment to avoid confusion for users expecting deep merge behavior.

🔎 Suggested documentation enhancement
 /**
  * Merge global config with version-specific config
  * Version-specific config takes precedence over global config
+ * Note: Nested objects (oauth, cors, openai, debug, protocol) are replaced entirely
+ * by version-specific values, not deep-merged.
  */
 function mergeVersionConfig<T extends ToolDefs>(

287-295: Dead code: version route can never conflict with /health.

The check if (versionRoute === "/health") is unreachable. Since versionRoute is constructed as /${versionKey}/mcp and versionKey must match /^v\d+$/, the resulting route (e.g., /v1/mcp) can never equal /health. This appears to be copy-paste from single-version validation.

🔎 Suggested removal
       validateVersionConfig(versionKey, versionConfig);
-
-      // Validate that version route doesn't conflict with reserved routes
-      const versionRoute = `/${versionKey}/mcp`;
-      if (versionRoute === "/health") {
-        throw new AppError(
-          ErrorCode.INVALID_CONFIG,
-          `Version "${versionKey}" route conflicts with health check endpoint`
-        );
-      }
     }

719-727: Consider stable cache key generation for OAuth config.

Using JSON.stringify directly is sensitive to property order. If the same OAuth configuration is defined with different property ordering, separate JWKS clients would be created unnecessarily. While this is unlikely in practice, a stable serialization approach would be more robust.

🔎 Suggested stable key generation
+    // Helper to create a stable cache key from OAuth config
+    function stableStringify(obj: unknown): string {
+      return JSON.stringify(obj, Object.keys(obj as object).sort());
+    }
+
     // Create version-specific OAuth JWKS client key (for reuse if config is identical)
     const oauthConfigKey = normalizedVersionConfig.config?.oauth
-      ? JSON.stringify(normalizedVersionConfig.config.oauth)
+      ? stableStringify(normalizedVersionConfig.config.oauth)
       : "no-oauth";

925-927: Clarify behavior: mainApp.tools returns first version's tools only.

For multi-version apps, mainApp.tools returns the first version's tool definitions, which may not represent all available tools across versions. Consider adding a JSDoc comment to clarify this or providing a method to access all versions' tools if needed.

🔎 Suggested documentation
   // Create main app instance that delegates to version apps
   const mainApp: App<T> = {
-    // Use tools from first version (for type inference)
+    // Use tools from first version (for type inference).
+    // To access a specific version's tools, use getVersion(key).tools
     tools: (Object.values(config.versions)[0] as VersionConfig<T> | undefined)?.tools as T,
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4d50c8 and b4f3d97.

📒 Files selected for processing (13)
  • README.md
  • examples/minimal/README.md
  • examples/minimal/src/index.ts
  • examples/minimal/src/ui/GreetingWidgetV1.tsx
  • examples/minimal/src/ui/GreetingWidgetV2.tsx
  • examples/minimal/src/ui/styles.css
  • packages/core/README.md
  • packages/core/src/createApp.ts
  • packages/core/src/server/index.ts
  • packages/core/src/server/oauth/middleware.ts
  • packages/core/src/types/config.ts
  • packages/core/src/types/tools.ts
  • packages/core/tests/unit/versioning.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript with no any types - use unknown and narrow instead

Files:

  • examples/minimal/src/ui/GreetingWidgetV2.tsx
  • packages/core/src/types/tools.ts
  • packages/core/src/types/config.ts
  • packages/core/tests/unit/versioning.test.ts
  • packages/core/src/server/index.ts
  • examples/minimal/src/index.ts
  • examples/minimal/src/ui/GreetingWidgetV1.tsx
  • packages/core/src/server/oauth/middleware.ts
  • packages/core/src/createApp.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Remove unused variables or prefix with underscore (_)

Files:

  • examples/minimal/src/ui/GreetingWidgetV2.tsx
  • packages/core/src/types/tools.ts
  • packages/core/src/types/config.ts
  • packages/core/tests/unit/versioning.test.ts
  • packages/core/src/server/index.ts
  • examples/minimal/src/index.ts
  • examples/minimal/src/ui/GreetingWidgetV1.tsx
  • packages/core/src/server/oauth/middleware.ts
  • packages/core/src/createApp.ts
{packages/core,examples}/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

{packages/core,examples}/**/*.ts: Always use defineTool and defineUI for type inference when defining tools and UI components
Use Koa-style async/await middleware pattern with await next() to chain middleware execution
Use AppError and ErrorCode from @mcp-apps-kit/core for error handling
Implement plugins using the Plugin interface with hooks: onInit, onStart, onShutdown, beforeToolCall, afterToolCall, onToolError
Use app.events.on() and app.events.once() for event subscription with event types like app:init, tool:call, app:start
Use Zod schemas with defineTool for input/output validation
Colocate UI definitions near tool definitions using defineUI with html property pointing to compiled UI assets

Files:

  • packages/core/src/types/tools.ts
  • packages/core/src/types/config.ts
  • packages/core/tests/unit/versioning.test.ts
  • packages/core/src/server/index.ts
  • examples/minimal/src/index.ts
  • packages/core/src/server/oauth/middleware.ts
  • packages/core/src/createApp.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use export type for type-only exports

Files:

  • packages/core/src/types/tools.ts
  • packages/core/src/types/config.ts
  • packages/core/tests/unit/versioning.test.ts
  • packages/core/src/server/index.ts
  • examples/minimal/src/index.ts
  • packages/core/src/server/oauth/middleware.ts
  • packages/core/src/createApp.ts
**/tests/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Mirror test file structure to source: tests in tests/ directory with unit/, integration/, and contract/ subdirectories

Files:

  • packages/core/tests/unit/versioning.test.ts
**/index.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Export public API only in index.ts files

Files:

  • packages/core/src/server/index.ts
  • examples/minimal/src/index.ts
🧠 Learnings (8)
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Always use `defineTool` and `defineUI` for type inference when defining tools and UI components

Applied to files:

  • README.md
  • packages/core/src/types/tools.ts
  • packages/core/src/types/config.ts
  • examples/minimal/src/index.ts
  • examples/minimal/README.md
  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Colocate UI definitions near tool definitions using `defineUI` with `html` property pointing to compiled UI assets

Applied to files:

  • README.md
  • examples/minimal/src/index.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Implement plugins using the `Plugin` interface with hooks: `onInit`, `onStart`, `onShutdown`, `beforeToolCall`, `afterToolCall`, `onToolError`

Applied to files:

  • packages/core/src/types/config.ts
  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/tests/**/*.{test,spec}.{ts,tsx} : Mirror test file structure to source: tests in `tests/` directory with `unit/`, `integration/`, and `contract/` subdirectories

Applied to files:

  • packages/core/tests/unit/versioning.test.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Remove unused variables or prefix with underscore (`_`)

Applied to files:

  • packages/core/tests/unit/versioning.test.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use Zod schemas with `defineTool` for input/output validation

Applied to files:

  • examples/minimal/src/index.ts
  • examples/minimal/README.md
  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use Koa-style async/await middleware pattern with `await next()` to chain middleware execution

Applied to files:

  • packages/core/src/server/oauth/middleware.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use `AppError` and `ErrorCode` from `mcp-apps-kit/core` for error handling

Applied to files:

  • packages/core/src/createApp.ts
🧬 Code graph analysis (7)
examples/minimal/src/ui/GreetingWidgetV2.tsx (1)
examples/minimal/src/index.ts (1)
  • AppClientToolsV2 (173-173)
packages/core/src/types/tools.ts (1)
packages/core/src/index.ts (1)
  • App (41-41)
packages/core/src/types/config.ts (4)
packages/core/src/types/tools.ts (1)
  • ToolDefs (332-332)
packages/core/src/index.ts (4)
  • ToolDefs (39-39)
  • UIDefs (50-50)
  • Plugin (75-75)
  • AppConfig (58-58)
packages/core/src/types/ui.ts (1)
  • UIDefs (141-141)
packages/core/src/plugins/types.ts (1)
  • Plugin (163-275)
packages/core/tests/unit/versioning.test.ts (1)
packages/core/src/createApp.ts (1)
  • createApp (413-423)
packages/core/src/server/index.ts (2)
packages/core/src/server/oauth/middleware.ts (1)
  • createOAuthMiddleware (166-225)
packages/core/src/utils/errors.ts (1)
  • wrapError (204-217)
examples/minimal/src/ui/GreetingWidgetV1.tsx (1)
examples/minimal/src/index.ts (1)
  • AppClientToolsV1 (167-167)
packages/core/src/server/oauth/middleware.ts (2)
packages/core/src/server/oauth/errors.ts (2)
  • OAuthError (7-128)
  • ErrorCode (133-151)
packages/core/src/server/oauth/jwt-verifier.ts (1)
  • verifyJWT (50-155)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🔇 Additional comments (33)
README.md (2)

65-65: LGTM! Clear feature addition.

The API Versioning feature is appropriately highlighted in the features list, aligning with the PR objectives.


185-248: Comprehensive API versioning documentation with clear examples.

The new section provides excellent guidance on multi-version API support. The TypeScript example clearly demonstrates:

  • Shared configuration across versions
  • Per-version tool definitions with distinct schemas
  • Version-specific config overrides
  • Programmatic access via getVersion and getVersions
  • Dedicated route exposure

The example accurately reflects the multi-version architecture introduced in this PR.

examples/minimal/src/ui/styles.css (3)

18-39: LGTM! Well-structured version badge UI.

The addition of position: relative to .container enables proper absolute positioning of the version badge. The badge styling with backdrop-filter and distinct v2 variant provides clear visual differentiation between API versions.


60-64: Clean styling for the full-name display.

The subtle typography and opacity create appropriate visual hierarchy within the greeting component.


136-149: Improved form layout with input-group.

The flexbox column layout with proper spacing and placeholder styling enhances the modal input experience for both V1 and V2 UIs.

packages/core/README.md (3)

217-267: Excellent foundational versioning documentation.

The basic usage section clearly demonstrates:

  • Shared config inheritance across versions
  • Per-version tool definitions with distinct schemas (v1 takes only name, v2 adds optional surname)
  • Proper route mapping documentation

The example effectively illustrates the core versioning capability.


391-403: Clear version key validation requirements.

The explicit pattern /^v\d+$/ with valid and invalid examples prevents confusion. This regex-based constraint ensures consistent route structure and parsing.


413-431: Important backward compatibility documentation.

Clearly documenting that getVersions() returns an empty array and getVersion() returns undefined for single-version apps ensures existing code continues to work without modification.

packages/core/src/types/tools.ts (1)

505-543: Well-defined versioning API surface.

The addition of getVersion and getVersions methods to the App interface provides clean programmatic access to version-specific app instances:

  • Return types are correct: getVersion returns App<T> | undefined (undefined for single-version apps or missing versions), and getVersions returns string[] (empty for single-version apps).
  • JSDoc is comprehensive: Examples clearly demonstrate usage patterns and return value expectations.
  • Type preservation: Both methods maintain the generic T extends ToolDefs parameter, ensuring type safety across version boundaries.

These additions align perfectly with the multi-version architecture documented across the PR.

packages/core/src/server/oauth/middleware.ts (1)

166-189: Solid implementation of lazy JWKS client initialization.

The enhanced OAuth middleware correctly supports lazy initialization through a getter function pattern:

  • Type-safe resolution: The function type guard typeof jwksClient === "function" safely identifies and resolves getters.
  • Proper error handling: Throws OAuthError with INVALID_REQUEST code when the resolved client is null, providing a clear diagnostic message.
  • Clean integration: The resolved client is correctly passed to verifyJWT on line 189, maintaining the existing verification flow.

This pattern enables per-version JWKS clients in multi-version deployments, as described in the PR context. The implementation follows the Koa-style async/await middleware pattern per coding guidelines.

packages/core/tests/unit/versioning.test.ts (1)

33-709: Comprehensive test coverage for versioning feature.

The test suite thoroughly covers multi-version app creation, version key validation, config merging, route isolation, tool execution isolation, backward compatibility, shared Express app references, and version-specific middleware. This provides strong confidence in the versioning implementation.

packages/core/src/types/config.ts (2)

240-278: Well-structured version configuration type.

The VersionConfig interface properly captures version-specific tools, UI, config overrides, and plugins with appropriate generics and documentation.


412-417: Clean union type for backward-compatible API.

AppConfigInput<T> elegantly supports both single-version (AppConfig) and multi-version (VersionsConfig) configurations, enabling seamless migration for existing users.

examples/minimal/src/ui/GreetingWidgetV2.tsx (1)

12-57: Clean component implementation with proper state management.

Good separation of concerns with local state for modal control, form inputs, loading/error states, and tool result caching. The greetOutput fallback pattern (greetResult ?? result?.greet) correctly prioritizes local state over hook result.

examples/minimal/README.md (1)

1-145: Comprehensive documentation update for versioning feature.

The README clearly explains the new versioning capabilities with:

  • Feature overview distinguishing v1 and v2 APIs
  • API endpoints table
  • Working curl examples for both versions
  • Updated Claude Desktop configuration
  • Project structure reflecting versioned UI components
  • Versioning configuration code example
packages/core/src/server/index.ts (3)

77-82: Well-designed API extension for versioned servers.

The updated signature supports:

  • Lazy JWKS client initialization via getter function
  • Custom version routes for multi-version deployments

Backward compatible — existing single-version apps work without changes.


147-152: Good lazy initialization pattern for JWKS client.

Normalizing the JWKS client to a getter function (getJwksClient) enables deferred initialization, which is useful when the client isn't needed immediately or when sharing across versions.


244-278: Correct conditional mounting of global endpoints.

Versioned servers delegate health checks, domain verification, and 404 handling to the shared parent Express app. This avoids duplicate endpoints and ensures consistent behavior across versions.

examples/minimal/src/ui/GreetingWidgetV1.tsx (1)

12-15: Clean version-specific refactoring.

Component correctly renamed to GreetingWidgetV1 with proper type narrowing to AppClientToolsV1. The version badge at line 54 provides clear visual identification.

examples/minimal/src/index.ts (4)

30-54: V1 tool follows coding guidelines.

Uses defineTool with Zod schemas for input/output validation and defineReactUI for colocated UI definition. Handler correctly returns structured output with _text for model narration.


71-97: V2 tool properly extends V1 capabilities.

Adds optional surname field and fullName output while maintaining the same patterns. Good demonstration of backward-compatible API evolution.


126-135: V2 uses different protocol — verify this is intentional.

V2 overrides protocol: "openai" while V1 uses the shared protocol: "mcp". This means V2 will use snake_case metadata format. If this is intentional for demonstration purposes, consider adding a code comment explaining why the protocols differ.


165-175: Well-organized type exports for UI consumers.

Separate exports for V1 and V2 types (AppToolsV1/AppClientToolsV1, AppToolsV2/AppClientToolsV2) enable type-safe UI development for each version.

packages/core/src/createApp.ts (10)

7-31: LGTM!

Imports are well-organized and appropriate for the new multi-version functionality. The separation of type imports using import type follows TypeScript best practices.


72-91: LGTM!

The type guard isVersionsConfig and version key validation are well-implemented. The regex pattern /^v\d+$/ clearly enforces the expected format (v1, v2, etc.), and error messages are descriptive.


93-128: LGTM!

Comprehensive validation of version-specific configuration with proper delegation to validateGlobalConfig for nested config validation. The error messages clearly identify which version has the issue.


457-522: LGTM!

The lazy OAuth initialization pattern is well-implemented with proper idempotency handling. The promise-based approach correctly handles concurrent initialization attempts by having subsequent callers await the in-progress promise.


639-652: LGTM!

The versioning method stubs for single-version apps correctly return undefined/empty array, maintaining a consistent API surface while indicating versioning is not applicable. The underscore prefix for _versionKey follows the coding guidelines for unused parameters.


1022-1063: LGTM!

The once implementation correctly ensures the handler fires only once across all versions, even with concurrent event emissions. Setting fired = true before calling the handler prevents race conditions, and the immediate unsubscribe loop prevents other versions from triggering duplicate calls.


979-999: LGTM!

The request routing logic correctly parses the URL path and delegates to the appropriate version app based on the /vN/mcp prefix pattern. The fallback to 404 for unmatched routes is consistent with the Express catch-all handler.


600-606: LGTM!

The handleRequest signature correctly uses globalThis.Request/globalThis.Response for Web API types and unknown for the optional env parameter, following strict TypeScript guidelines.


903-922: LGTM!

The shared Express app correctly adds:

  1. Health endpoint with version listing
  2. OpenAI domain verification challenge (when configured)
  3. Catch-all 404 handler

These endpoints are appropriately placed after version-specific routes are mounted.


960-971: No action required; type casting works as designed.

The return statement safely casts ServerInstance to McpServer without runtime issues. Since McpServer is an opaque interface accepting any properties ([key: string]: unknown), consumers can access httpServer and other ServerInstance properties without errors. Tests throughout the codebase confirm this pattern works correctly.

Likely an incorrect or invalid review comment.

Comment thread examples/minimal/src/ui/GreetingWidgetV2.tsx Outdated
Comment thread packages/core/src/createApp.ts
@gabrypavanello gabrypavanello self-assigned this Jan 5, 2026
…App comments

- Removed unnecessary surname check in GreetingWidgetV2 onKeyDown event.
- Updated comments in createApp.ts for clarity on version-specific config merging and server instance handling.
- Cleaned up versioning test file by removing unused server cleanup logic.
@gabrypavanello gabrypavanello changed the title Versioning feat((versioning): Implemented versioned route apps Jan 5, 2026
* and backward compatibility with single-version apps.
*/

import { describe, it, expect, afterEach } from "vitest";
@gabrypavanello

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor

cursor Bot commented Jan 5, 2026

Copy link
Copy Markdown

Skipping Bugbot: Bugbot is disabled for this repository

@gabrypavanello

Copy link
Copy Markdown
Contributor Author

@cursor review

@claude

claude Bot commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Code Review: API Versioning Feature

I've completed a thorough review of PR #63 which adds API versioning support to mcp-apps-kit. This is a well-architected feature with solid implementation.


✅ Strengths

1. Excellent Architecture & Design

  • Clean separation between single-version and multi-version apps with backward compatibility
  • Smart config merging strategy (version-specific overrides global)
  • Proper route isolation with shared Express app
  • OAuth JWKS client reuse for identical configs (performance optimization at lines 712-734 in createApp.ts)

2. Robust Validation

  • Comprehensive runtime validation of version keys
  • Config validation for both global and version-specific settings
  • Good error messages with context

3. Strong Test Coverage

  • 691 lines of comprehensive unit tests (versioning.test.ts)
  • Tests cover: config merging, route isolation, tool execution, middleware, events, backward compatibility
  • Integration tests using actual MCP clients and HTTP requests

4. Documentation Quality

  • Extensive README updates with clear examples
  • Well-documented API in inline comments
  • Good examples in minimal/ showing v1 vs v2 differences

🔍 Issues & Concerns

CRITICAL: Event Handler Memory Leak (High Priority)

Location: createApp.ts:1015-1056

The once() implementation for multi-version apps has a race condition that can cause memory leaks. If version A fires the event before version B registers, version B's wrapper stays attached but will never execute (due to fired flag). The handler stays in memory indefinitely.

Fix: Unsubscribe from all versions BEFORE calling the handler to prevent race conditions.


MAJOR: Missing Version Validation (Medium Priority)

Location: createApp.ts:972-992

The handleRequest() method for serverless doesn't validate that the version key exists before routing. Valid version pattern (v999) but non-existent version returns generic 404. Should return explicit Version not found error with available versions.


MINOR Issues:

  1. Inconsistent Error Handling (createApp.ts:469-515, 780-783) - OAuth initialization should also reset jwksClient on failure
  2. Type Safety Gap (createApp.ts:921) - Double type assertion defeats TypeScript's safety
  3. Missing Test Cleanup (versioning.test.ts) - Several tests don't clean up HTTP servers in error paths

📊 Testing Assessment

Coverage: Excellent (691 new test lines)

Missing Tests:

  • OAuth config reuse
  • Error paths in OAuth initialization
  • handleRequest() with invalid version keys
  • Event system edge cases (especially once() race conditions)

🔒 Security Review

✅ No security vulnerabilities detected

  • OAuth validation properly isolated per version
  • CORS config correctly merged and applied
  • No injection risks in route parsing
  • Input validation maintained for all versions

📝 Recommendation Summary

Priority Issue Lines Action
CRITICAL Event handler memory leak 1015-1056 Fix race condition in once()
MAJOR Missing version validation 972-992 Add explicit error for non-existent versions
MINOR OAuth cleanup on error 469-515, 780-783 Reset jwksClient on failure

✅ Approval Status

Conditional Approval - Fix the CRITICAL issue before merge.

This is a well-designed feature that adds significant value to the framework. The implementation is solid, documentation is excellent, and test coverage is strong. However, the event handler race condition in once() must be addressed before merging to prevent memory leaks in production.

After fixing the critical issue, this will be a great addition to mcp-apps-kit! 🎉

Great work overall! The versioning architecture is clean and maintainable. 🚀

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Fix all issues with AI Agents 🤖
In @packages/core/tests/unit/versioning.test.ts:
- Line 8: The test file imports afterEach but never uses it; remove the unused
symbol by updating the import statement that currently reads "import { describe,
it, expect, afterEach } from 'vitest';" to drop afterEach so it becomes "import
{ describe, it, expect } from 'vitest';" and ensure no other references to
afterEach exist in versioning.test.ts.
🧹 Nitpick comments (2)
packages/core/src/createApp.ts (1)

712-716: Consider stable cache key generation for OAuth config.

Using JSON.stringify for the OAuth config cache key (line 714) could theoretically produce different keys if property ordering varies, though this is unlikely in practice since configs come from the same source.

Alternative approach using sorted keys
// Helper function to create stable cache key
function createOAuthCacheKey(oauthConfig: OAuthConfig): string {
  const keys = Object.keys(oauthConfig).sort();
  const parts = keys.map(k => `${k}:${JSON.stringify(oauthConfig[k as keyof OAuthConfig])}`);
  return parts.join('|');
}

// Usage
const oauthConfigKey = normalizedVersionConfig.config?.oauth
  ? createOAuthCacheKey(normalizedVersionConfig.config.oauth)
  : "no-oauth";
packages/core/tests/unit/versioning.test.ts (1)

365-370: Consider extracting server cleanup into a helper.

The manual server cleanup pattern is repeated across multiple tests. While explicit and correct, extracting it into a helper function would reduce duplication.

Example helper function
async function closeServer(app: ReturnType<typeof createApp>): Promise<void> {
  const httpServer = app.getServer().httpServer;
  if (httpServer) {
    await new Promise<void>((resolve) => {
      httpServer.close(() => resolve());
    });
  }
}

// Usage in tests:
await closeServer(app);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4f3d97 and 0233cd6.

📒 Files selected for processing (3)
  • examples/minimal/src/ui/GreetingWidgetV2.tsx
  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/minimal/src/ui/GreetingWidgetV2.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript with no any types - use unknown and narrow instead

Files:

  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Remove unused variables or prefix with underscore (_)

Files:

  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
{packages/core,examples}/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

{packages/core,examples}/**/*.ts: Always use defineTool and defineUI for type inference when defining tools and UI components
Use Koa-style async/await middleware pattern with await next() to chain middleware execution
Use AppError and ErrorCode from @mcp-apps-kit/core for error handling
Implement plugins using the Plugin interface with hooks: onInit, onStart, onShutdown, beforeToolCall, afterToolCall, onToolError
Use app.events.on() and app.events.once() for event subscription with event types like app:init, tool:call, app:start
Use Zod schemas with defineTool for input/output validation
Colocate UI definitions near tool definitions using defineUI with html property pointing to compiled UI assets

Files:

  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use export type for type-only exports

Files:

  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
**/tests/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Mirror test file structure to source: tests in tests/ directory with unit/, integration/, and contract/ subdirectories

Files:

  • packages/core/tests/unit/versioning.test.ts
🧠 Learnings (6)
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Always use `defineTool` and `defineUI` for type inference when defining tools and UI components

Applied to files:

  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use Zod schemas with `defineTool` for input/output validation

Applied to files:

  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use `AppError` and `ErrorCode` from `mcp-apps-kit/core` for error handling

Applied to files:

  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Implement plugins using the `Plugin` interface with hooks: `onInit`, `onStart`, `onShutdown`, `beforeToolCall`, `afterToolCall`, `onToolError`

Applied to files:

  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/tests/**/*.{test,spec}.{ts,tsx} : Mirror test file structure to source: tests in `tests/` directory with `unit/`, `integration/`, and `contract/` subdirectories

Applied to files:

  • packages/core/tests/unit/versioning.test.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Remove unused variables or prefix with underscore (`_`)

Applied to files:

  • packages/core/tests/unit/versioning.test.ts
🧬 Code graph analysis (1)
packages/core/tests/unit/versioning.test.ts (1)
packages/core/src/createApp.ts (1)
  • createApp (406-416)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🔇 Additional comments (12)
packages/core/src/createApp.ts (9)

8-31: LGTM!

The type imports properly support the multi-version architecture. All imports are necessary for the new functionality.


75-79: LGTM!

The type guard correctly discriminates between single-version and multi-version configurations using a property check.


84-321: LGTM!

The validation functions provide comprehensive runtime checks for both single-version and multi-version configurations. Proper use of AppError and ErrorCode for error handling, and good integration with Zod schemas for OAuth validation.


329-357: LGTM!

The config merging logic correctly prioritizes version-specific config over global config. The shallow merge approach for nested objects (line 339-343) and plugin array concatenation (line 347) align with the documented behavior.


406-416: LGTM!

The entry point correctly delegates to version-specific implementations based on config shape. Clean separation of concerns.


421-653: LGTM!

The single-version implementation maintains backward compatibility while extending the API with stub implementations of getVersion and getVersions (lines 636-645). The lazy OAuth initialization pattern (line 520) is properly documented.


940-943: Past issue resolved: HTTP server now attached to all version instances.

The shared HTTP server is correctly attached to all version ServerInstance objects, addressing the previous review feedback. This ensures getServer().httpServer works consistently across all versions.


1015-1056: LGTM: Complex but correct once() implementation.

The multi-version once() implementation correctly ensures the handler fires exactly once across all versions by using shared state (fired and isUnsubscribed flags) and immediately unsubscribing from all versions after the first execution. The complexity is justified by the multi-version coordination requirement.


658-1081: LGTM: Well-structured multi-version orchestration.

The implementation provides:

  • Shared Express app with per-version routing and middleware isolation (lines 660, 811)
  • OAuth JWKS client caching and reuse across versions with identical configs (lines 672-674)
  • Lazy OAuth initialization with proper promise management (lines 724-788)
  • Comprehensive multi-version event handling (lines 1002-1069)
  • Shared health and OpenAI challenge endpoints (lines 896-910)
packages/core/tests/unit/versioning.test.ts (3)

14-440: LGTM: Comprehensive test coverage for versioning features.

The tests thoroughly exercise multi-version app creation, version APIs, validation, config merging, and route isolation. Each test properly cleans up HTTP servers to avoid resource leaks.


442-526: LGTM: Tool execution isolation properly tested.

The test validates that tools with the same name but different signatures execute independently per version, confirming proper isolation.


528-691: LGTM: Backward compatibility and middleware isolation well tested.

The tests ensure:

  • Single-version apps maintain backward compatibility (lines 528-568)
  • All versions share the same Express app instance (lines 570-595)
  • Version-specific middleware executes in isolation (lines 597-690)

Comment thread packages/core/tests/unit/versioning.test.ts
Comment thread packages/core/src/createApp.ts
…eApp

- Implemented a health check endpoint that returns the app status and available versions.
- Added support for handling OpenAI domain verification challenge requests.
- Updated routing logic to return 404 for unmatched routes, ensuring consistency with Express behavior.
- Enhanced comments for clarity on event handling and memory management in the request processing flow.
@claude

claude Bot commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Code Review: API Versioning Feature

I've completed a thorough review of this PR. Overall, this is a well-designed and thoughtfully implemented feature that adds significant value to the framework. The code quality is high, tests are comprehensive, and documentation is excellent.


✅ Strengths

Architecture & Design

  • Clean abstraction: The distinction between single-version and multi-version apps is well-handled with proper backward compatibility
  • Excellent separation of concerns: Version isolation is maintained while sharing common infrastructure
  • Smart config merging: Version-specific configs properly override global configs with clear precedence rules
  • Lazy initialization: OAuth JWKS clients are initialized on-demand, supporting serverless deployments

Code Quality

  • Type safety: Strong TypeScript usage throughout with proper type inference
  • Validation: Comprehensive runtime validation for version keys, config structure, and route conflicts
  • Error handling: Proper error boundaries and informative error messages
  • Clean code patterns: Good use of helper functions, consistent naming, clear comments

Testing

  • Comprehensive coverage: 806 lines of tests covering version creation, config merging, route isolation, middleware, and backward compatibility
  • Integration tests: Real HTTP server tests verify end-to-end functionality
  • Edge cases: Tests cover 404 handling, invalid version keys, and tool execution isolation

Documentation

  • Excellent README updates: Clear examples for both single and multi-version apps
  • Updated examples: The minimal example demonstrates versioning patterns effectively
  • API documentation: Type definitions include helpful JSDoc comments

🔍 Issues & Concerns

1. Memory Management - HTTP Server Leak Risk ⚠️ HIGH PRIORITY

Location: createApp.ts:938-950

The shared HTTP server is created but there's no cleanup mechanism. No app.stop() method for multi-version apps. Risk of port conflicts and resource leaks in test suites.

Recommendation: Add a stop() method to the multi-version app that properly closes the shared HTTP server.


2. Config Merging Shallow vs Deep 🤔 MEDIUM PRIORITY

Location: createApp.ts:329-344

The comment states nested objects are replaced entirely, not deep-merged. This could be surprising behavior where version-specific OAuth configs completely replace global OAuth configs, losing shared properties like scopes.

Recommendation: Document this behavior more prominently in the README with a clear example, or consider deep merging for nested objects like OAuth/CORS configs.


3. Type Safety Gap in Multi-Version Tools ⚠️ MEDIUM PRIORITY

Location: createApp.ts:919-921

The main app tools property uses the first version tools with a type assertion. This is problematic because different versions may have different tool types.

Recommendation: Make tools optional/undefined for multi-version apps to force users to use getVersion(), or add a deprecation notice.


4. OAuth JWKS Client Reuse Logic ⚠️ MEDIUM PRIORITY

Location: createApp.ts:713-787

The OAuth client reuse is based on JSON stringifying the config. JSON.stringify is order-dependent - identical configs with different key orders will create separate clients. The tokenVerifier function won't serialize properly.

Recommendation: Use a more robust hashing approach with sorted keys.


5. Test Cleanup and Port Management ⚠️ MEDIUM PRIORITY

Location: versioning.test.ts (multiple locations)

Tests use hardcoded ports which can cause port conflicts if tests run in parallel. Cleanup logic is repetitive and incomplete.

Recommendation: Use dynamic port allocation, add proper afterEach hooks, and create a test helper function for server lifecycle management.


📊 Overall Assessment

Rating: ⭐⭐⭐⭐½ (4.5/5)

This is a high-quality feature implementation that follows the framework conventions and maintains backward compatibility. The main concerns are around resource management and some edge cases in config merging and OAuth client reuse.

Recommendation: Approve with minor changes

Must fix before merge:

  • Add stop() method for multi-version apps
  • Improve test cleanup with proper afterEach hooks

Should fix (can be follow-up PRs):

  • Document config merging behavior
  • Improve OAuth client reuse hashing
  • Add more edge case tests

Great work on this feature! The versioning support is a valuable addition to the framework. 🚀

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Fix all issues with AI Agents 🤖
In @packages/core/tests/unit/versioning.test.ts:
- Around line 75-84: The test name is misleading: it says "should return
undefined for getVersions()" but the assertion checks for an empty array; update
the test description to reflect expected behavior. Change the it(...) string in
the versioning.test.ts case that calls app.getVersions() to something like
"should return an empty array for getVersions() in single-version mode" so the
test name matches the assertion and implementation (refer to the test invoking
createApp(...) and app.getVersions()).
♻️ Duplicate comments (1)
packages/core/tests/unit/versioning.test.ts (1)

8-8: Remove unused afterEach import.

The afterEach import is never used in this test file. As per coding guidelines, unused imports should be removed.

🔎 Suggested fix
-import { describe, it, expect, afterEach } from "vitest";
+import { describe, it, expect } from "vitest";
🧹 Nitpick comments (1)
packages/core/tests/unit/versioning.test.ts (1)

210-289: Config merging tests lack behavioral verification.

These tests create apps with merged configurations but only assert that the app is defined. The comments indicate intent to verify config override behavior (e.g., "Version-specific config should override global"), but there's no actual verification. Consider adding assertions that confirm the merged config is applied correctly, for example by checking observable behavior affected by the config.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0233cd6 and a3e9c0c.

📒 Files selected for processing (2)
  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript with no any types - use unknown and narrow instead

Files:

  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Remove unused variables or prefix with underscore (_)

Files:

  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
{packages/core,examples}/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

{packages/core,examples}/**/*.ts: Always use defineTool and defineUI for type inference when defining tools and UI components
Use Koa-style async/await middleware pattern with await next() to chain middleware execution
Use AppError and ErrorCode from @mcp-apps-kit/core for error handling
Implement plugins using the Plugin interface with hooks: onInit, onStart, onShutdown, beforeToolCall, afterToolCall, onToolError
Use app.events.on() and app.events.once() for event subscription with event types like app:init, tool:call, app:start
Use Zod schemas with defineTool for input/output validation
Colocate UI definitions near tool definitions using defineUI with html property pointing to compiled UI assets

Files:

  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use export type for type-only exports

Files:

  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
**/tests/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Mirror test file structure to source: tests in tests/ directory with unit/, integration/, and contract/ subdirectories

Files:

  • packages/core/tests/unit/versioning.test.ts
🧠 Learnings (6)
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Always use `defineTool` and `defineUI` for type inference when defining tools and UI components

Applied to files:

  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use Zod schemas with `defineTool` for input/output validation

Applied to files:

  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use `AppError` and `ErrorCode` from `mcp-apps-kit/core` for error handling

Applied to files:

  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Implement plugins using the `Plugin` interface with hooks: `onInit`, `onStart`, `onShutdown`, `beforeToolCall`, `afterToolCall`, `onToolError`

Applied to files:

  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/tests/**/*.{test,spec}.{ts,tsx} : Mirror test file structure to source: tests in `tests/` directory with `unit/`, `integration/`, and `contract/` subdirectories

Applied to files:

  • packages/core/tests/unit/versioning.test.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Remove unused variables or prefix with underscore (`_`)

Applied to files:

  • packages/core/tests/unit/versioning.test.ts
🧬 Code graph analysis (1)
packages/core/tests/unit/versioning.test.ts (1)
packages/core/src/createApp.ts (1)
  • createApp (406-416)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🔇 Additional comments (10)
packages/core/tests/unit/versioning.test.ts (1)

1-806: Comprehensive test coverage for versioning feature.

The test suite thoroughly covers the versioning functionality including multi-version creation, version key validation, route isolation, tool execution isolation, backward compatibility, and serverless deployment scenarios. The test structure is clean and follows good practices.

packages/core/src/createApp.ts (9)

84-91: LGTM!

The version key validation correctly enforces the /^v\d+$/ pattern with a clear error message.


329-357: LGTM!

The config merging logic correctly handles version-specific overrides with well-documented shallow merge behavior for nested objects. The empty name is appropriately set from global config at the call site.


421-653: LGTM!

Single-version app creation properly maintains backward compatibility with lazy OAuth initialization and correct handling of getVersion/getVersions methods.


712-788: OAuth client caching is well-implemented.

The lazy OAuth initialization with per-config caching via JSON.stringify is a pragmatic approach that correctly handles:

  • Reusing clients when configs are identical
  • Waiting for in-progress initialization
  • Cleaning up on failure to allow retry

805-811: LGTM!

The Express app mounting correctly composes version-specific routers onto the shared app, maintaining route isolation between versions.


972-1020: Shared endpoint handling in handleRequest is complete.

The implementation correctly handles /health and /.well-known/openai-apps-challenge endpoints in the serverless handleRequest path, addressing the concern from the previous review.


1043-1086: Well-designed once() implementation with proper race condition handling.

The implementation correctly handles the "fire only once across all versions" semantics with:

  • Early fired flag set before unsubscribing to prevent races
  • Cleanup before handler execution to prevent memory leaks
  • Guard against duplicate unsubscribe calls

The comments clearly explain the design rationale.


939-944: HTTP server correctly attached to all version instances.

The implementation now properly attaches the shared HTTP server to all version ServerInstance objects, addressing the previous review concern about only attaching to the first version.


658-1111: Solid multi-version app implementation.

The createMultiVersionApp function is well-structured with:

  • Proper sharing of Express app and HTTP server across versions
  • Efficient OAuth client caching by config hash
  • Correct event propagation semantics for on, once, and onAny
  • Comprehensive handleRequest support for serverless deployments
  • Clear documentation of design decisions in comments

Comment on lines +75 to +84
it("should return undefined for getVersions() in single-version mode", () => {
const app = createApp({
name: "test-app",
version: "1.0.0",
tools: {},
});

const versions = app.getVersions();
expect(versions).toEqual([]);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix misleading test name.

The test name says "should return undefined for getVersions()" but the assertion expects an empty array [], not undefined. The assertion is correct based on the implementation, but the test name is misleading.

🔎 Suggested fix
-    it("should return undefined for getVersions() in single-version mode", () => {
+    it("should return empty array for getVersions() in single-version mode", () => {
📝 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.

Suggested change
it("should return undefined for getVersions() in single-version mode", () => {
const app = createApp({
name: "test-app",
version: "1.0.0",
tools: {},
});
const versions = app.getVersions();
expect(versions).toEqual([]);
});
it("should return empty array for getVersions() in single-version mode", () => {
const app = createApp({
name: "test-app",
version: "1.0.0",
tools: {},
});
const versions = app.getVersions();
expect(versions).toEqual([]);
});
🤖 Prompt for AI Agents
In @packages/core/tests/unit/versioning.test.ts around lines 75-84, The test
name is misleading: it says "should return undefined for getVersions()" but the
assertion checks for an empty array; update the test description to reflect
expected behavior. Change the it(...) string in the versioning.test.ts case that
calls app.getVersions() to something like "should return an empty array for
getVersions() in single-version mode" so the test name matches the assertion and
implementation (refer to the test invoking createApp(...) and
app.getVersions()).

- Updated `validateGlobalConfig` to accept `VersionSpecificConfig`, allowing null values to disable properties.
- Introduced `deepMerge` function for recursively merging global and version-specific configurations, with support for null to remove properties.
- Updated `mergeVersionConfig` to utilize deep merging for nested objects, ensuring proper handling of undefined and null values.
- Added tests for deep merging behavior, including scenarios for overriding, disabling, and inheriting configurations.
- Enhanced type definitions for better clarity on configuration structures.
@claude

claude Bot commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Code Review: API Versioning Implementation

Summary

This PR implements a comprehensive API versioning feature for @mcp-apps-kit/core, allowing multiple API versions to be exposed from a single application. The implementation is well-architected with good test coverage and maintains backward compatibility.

✅ Strengths

1. Architecture & Design

  • Clean separation: Multi-version and single-version apps are handled through separate code paths while sharing core infrastructure
  • Shared Express app: Efficient use of a single Express instance for all versions reduces memory overhead
  • Lazy OAuth initialization: Smart use of getter functions for JWKS clients enables proper initialization in both server and serverless contexts
  • Deep merge strategy: Well-thought-out config merging with null/undefined semantics (null = disable, undefined = inherit)

2. Type Safety

  • Strong TypeScript usage throughout with proper generic constraints
  • Version-specific type exports in examples (AppClientToolsV1, AppClientToolsV2)
  • Good use of type guards (isVersionsConfig, isUIDef)

3. Testing

  • Comprehensive test suite covering:
    • Multi-version creation and isolation
    • Config merging (deep merge, null handling, array replacement)
    • Route isolation and tool execution
    • Backward compatibility
    • Serverless (handleRequest) scenarios
  • 967 lines of well-structured tests demonstrate thorough validation

4. Documentation

  • Excellent documentation updates in README and package README
  • Clear examples showing v1 vs v2 differences
  • Well-documented minimal example with versioning

5. Error Handling

  • Proper validation of version keys (/^v\d+$/)
  • Conflict detection for reserved routes (/health)
  • Clear error messages with available versions when accessing non-existent versions

🔍 Issues & Concerns

Critical Issues

1. Version Key Constraint Too Restrictive

Location: packages/core/src/createApp.ts:84-91

function validateVersionKey(versionKey: string): void {
  if (!/^v\d+$/.test(versionKey)) {
    throw new AppError(
      ErrorCode.INVALID_CONFIG,
      `Version key must match pattern /^v\\d+$/, got: "${versionKey}"`
    );
  }
}

Issue: The pattern /^v\d+$/ only allows simple numeric versions (v1, v2, v10) but prevents common versioning patterns:

  • v1.0, v2.1 (minor versions)
  • v1-beta, v2-alpha (pre-release versions)
  • v2023, v2024 (date-based versions)

Impact: Limits flexibility for real-world API versioning strategies.

Recommendation: Consider relaxing to /^v\d+(\.\d+)?(-\w+)?$/ or document the rationale for the restrictive pattern. If intentional, add a comment explaining why only major versions are supported.

2. OAuth JWKS Client Shared Across Versions

Location: packages/core/src/createApp.ts:803-855

// Create version-specific OAuth JWKS client key (for reuse if config is identical)
const oauthConfigKey = normalizedVersionConfig.config?.oauth
  ? JSON.stringify(normalizedVersionConfig.config.oauth)
  : "no-oauth";

Issue: Using JSON.stringify() for cache key generation has risks:

  • Property order sensitivity: {a:1, b:2} !== {b:2, a:1} in JSON.stringify
  • Function serialization: Loses tokenVerifier function if present
  • Nested object issues: May not handle complex nested structures consistently

Example failure case:

// These should be identical but create different keys:
{ authorizationServer: "https://auth.com", scopes: ["read"] }
{ scopes: ["read"], authorizationServer: "https://auth.com" }

Recommendation: Use a stable hash function or compare normalized config objects:

const oauthConfigKey = normalizedVersionConfig.config?.oauth
  ? crypto
      .createHash('sha256')
      .update(
        JSON.stringify(
          Object.keys(config.oauth)
            .sort()
            .reduce((acc, key) => ({ ...acc, [key]: config.oauth[key] }), {})
        )
      )
      .digest('hex')
  : "no-oauth";

3. Missing Validation for serverRoute in Versioned Config

Location: packages/core/src/createApp.ts:139-156

if (serverRoute === "/health") {
  throw new AppError(
    ErrorCode.INVALID_CONFIG,
    `${prefix}.serverRoute cannot be "/health" as it conflicts with the health check endpoint`
  );
}

Issue: The validation prevents /health conflict, but multi-version apps use /{versionKey}/mcp routes that are hardcoded. If a version tries to override serverRoute, it will be ignored but no warning is issued.

Recommendation: Add validation or warning when serverRoute is specified in version-specific config:

if (versionConfig.config?.serverRoute !== undefined) {
  throw new AppError(
    ErrorCode.INVALID_CONFIG,
    `Version "${versionKey}".config.serverRoute is not supported in multi-version apps. Routes are automatically assigned as /{versionKey}/mcp`
  );
}

Medium Issues

4. Debug Logger Not Configured Per-Version

Location: packages/core/src/createApp.ts:788-791

// Note: Debug logger is configured once with global config (line 681-683).
// We don't reconfigure it per-version because it's a global singleton.

Issue: This is documented but may lead to unexpected behavior. If v1 has debug.level: "error" and v2 has debug.level: "debug", the global logger will use whichever was configured first.

Recommendation: Either:

  1. Document this limitation prominently in the versioning docs
  2. Implement per-version logger instances (breaking change)
  3. Merge debug configs and use the most verbose level across all versions

5. Type Inference Issue for Multi-Version Apps

Location: packages/core/src/createApp.ts:1010-1012

tools: (Object.values(config.versions)[0] as VersionConfig<T> | undefined)?.tools as T,

Issue: The main app's tools property uses the first version's tools for type inference. This is confusing because:

  • It's arbitrary (depends on object key order)
  • Users might expect to access all versions' tools
  • The cast to T loses type safety

Recommendation: Document this clearly or provide a discriminated union type for multi-version tool access:

// Better API:
const v1App = app.getVersion("v1");
const result = await v1App.tools.greet({ name: "Alice" }); // Type-safe

6. Event Deduplication in once()

Location: packages/core/src/createApp.ts:1146-1189

Issue: The once() implementation for multi-version apps is complex and has race condition considerations. While the current implementation looks correct, it's prone to subtle bugs.

Observation: Good defensive programming with the isUnsubscribed flag to prevent race conditions. However, the complexity suggests this might be a code smell.

Recommendation: Add integration tests specifically for concurrent event firing across versions to validate the race condition handling.

Minor Issues

7. Inconsistent Error Messages

// packages/core/src/createApp.ts:102
`Version "${versionKey}".version is required and must be a non-empty string`

// packages/core/src/createApp.ts:109
`Version "${versionKey}".tools is required and must be an object`

Issue: Inconsistent use of .version vs .tools in error messages could be confused with the version string itself.

Recommendation: Use bracket notation for clarity:

`Version "${versionKey}"["version"] is required...`

8. Missing JSDoc for Public Methods

Location: packages/core/src/createApp.ts:726-736

getVersion: (_versionKey: string): App<T> | undefined => {
  return undefined;
},

Issue: Public API methods getVersion() and getVersions() lack JSDoc documentation in the implementation.

Recommendation: Add JSDoc comments to match the style used elsewhere in the codebase.

9. Hardcoded Port in Tests

Location: packages/core/tests/unit/versioning.test.ts (multiple locations)

const port = 3100; // 3101, 3102, 3103, 3104, 3105...

Issue: Sequential hardcoded ports could cause test flakiness if:

  • Tests run in parallel
  • Ports are already in use on CI
  • Tests don't clean up properly

Recommendation: Use port 0 to let the OS assign random available ports:

const port = 0; // Let OS assign
await app.start({ port });
const actualPort = app.getServer().httpServer?.address().port;

10. Missing Test for Config Override Precedence

The tests cover deep merge scenarios but don't test the precedence explicitly when both global and version configs exist for complex scenarios.

Recommendation: Add a test showing explicit precedence:

it("should give version config precedence over global config", () => {
  // Global says protocol: "mcp", cors.origin: true
  // v1 says protocol: "openai", cors.credentials: true
  // Verify v1 has: protocol="openai", cors.origin=true, cors.credentials=true
});

🔒 Security Considerations

Well Handled

  1. OAuth isolation: Each version can have independent OAuth configuration
  2. JWKS client reuse: Proper caching prevents redundant network requests
  3. Bearer token validation: Consistent validation across versions
  4. Route isolation: Versions cannot interfere with each other's tools

⚠️ Considerations

  1. JWKS cache sharing: Versions with different authorizationServer URLs share JWKS client cache. While efficient, ensure this doesn't leak information if versions should be isolated.
  2. Error messages: Version-not-found errors expose all available versions (availableVersions). This is fine for most use cases but could be information disclosure in high-security environments.

🚀 Performance Considerations

Well Optimized

  1. Single Express app: Avoids HTTP server overhead per version
  2. Shared middleware: Global middleware registered once, not per-version
  3. Lazy OAuth init: Defers JWKS discovery until first request in serverless
  4. JWKS client pooling: Reuses clients for identical OAuth configs

💡 Potential Optimizations

  1. Plugin initialization: Currently sequential (await in loop at line 1020). Consider Promise.all() for parallel initialization
  2. Event subscriptions: Multi-version apps subscribe to all versions' events. For high-volume apps, this could create many listeners

📋 Recommendations Summary

Must Fix Before Merge

  1. ⚠️ Fix OAuth config cache key generation (use stable hash)
  2. ⚠️ Add validation/warning for version-specific serverRoute overrides

Should Fix

  1. Document debug logger global singleton limitation
  2. Document or improve main app tools type inference for multi-version
  3. Consider relaxing version key pattern or document rationale

Nice to Have

  1. Use dynamic ports in tests (port 0)
  2. Add integration tests for event race conditions
  3. Add explicit config precedence test
  4. Add JSDoc to public methods
  5. Consider parallel plugin initialization

✨ Overall Assessment

Grade: A- (Excellent with minor issues)

This is a well-implemented feature with:

  • ✅ Strong architecture and design patterns
  • ✅ Comprehensive test coverage (967 lines)
  • ✅ Excellent documentation
  • ✅ Backward compatibility maintained
  • ✅ Good TypeScript usage
  • ⚠️ A few fixable issues around config hashing and validation

The implementation shows maturity and production-readiness. The critical issues are fixable with small changes and don't represent fundamental design flaws.

Recommendation: Request changes for the two critical issues, then approve after fixes.


Great work on this feature! The versioning system is well-thought-out and will be valuable for evolving APIs. 🎉

…ation

- Eliminated example curl commands from the index.ts file to streamline the documentation.
- Focused on providing a clearer overview of available endpoints without cluttering the content.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/createApp.ts (1)

1-1: Fix Prettier formatting to resolve pipeline failure.

The CI pipeline is failing due to formatting issues. Run prettier --write packages/core/src/createApp.ts to fix.

♻️ Duplicate comments (2)
packages/core/tests/unit/versioning.test.ts (2)

8-8: Remove unused import.

The afterEach import is unused. Tests perform manual cleanup inline rather than using an afterEach hook.

As per coding guidelines, unused imports should be removed.


75-84: Fix misleading test name.

The test name says "should return undefined for getVersions()" but the assertion expects an empty array [], not undefined. The assertion is correct based on the implementation, but the test name is misleading.

🔎 Suggested fix
-    it("should return undefined for getVersions() in single-version mode", () => {
+    it("should return empty array for getVersions() in single-version mode", () => {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3e9c0c and e613e09.

📒 Files selected for processing (4)
  • packages/core/src/createApp.ts
  • packages/core/src/index.ts
  • packages/core/src/types/config.ts
  • packages/core/tests/unit/versioning.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript with no any types - use unknown and narrow instead

Files:

  • packages/core/src/index.ts
  • packages/core/src/types/config.ts
  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Remove unused variables or prefix with underscore (_)

Files:

  • packages/core/src/index.ts
  • packages/core/src/types/config.ts
  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
**/index.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Export public API only in index.ts files

Files:

  • packages/core/src/index.ts
{packages/core,examples}/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

{packages/core,examples}/**/*.ts: Always use defineTool and defineUI for type inference when defining tools and UI components
Use Koa-style async/await middleware pattern with await next() to chain middleware execution
Use AppError and ErrorCode from @mcp-apps-kit/core for error handling
Implement plugins using the Plugin interface with hooks: onInit, onStart, onShutdown, beforeToolCall, afterToolCall, onToolError
Use app.events.on() and app.events.once() for event subscription with event types like app:init, tool:call, app:start
Use Zod schemas with defineTool for input/output validation
Colocate UI definitions near tool definitions using defineUI with html property pointing to compiled UI assets

Files:

  • packages/core/src/index.ts
  • packages/core/src/types/config.ts
  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use export type for type-only exports

Files:

  • packages/core/src/index.ts
  • packages/core/src/types/config.ts
  • packages/core/src/createApp.ts
  • packages/core/tests/unit/versioning.test.ts
**/tests/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Mirror test file structure to source: tests in tests/ directory with unit/, integration/, and contract/ subdirectories

Files:

  • packages/core/tests/unit/versioning.test.ts
🧠 Learnings (7)
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Always use `defineTool` and `defineUI` for type inference when defining tools and UI components

Applied to files:

  • packages/core/src/types/config.ts
  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Colocate UI definitions near tool definitions using `defineUI` with `html` property pointing to compiled UI assets

Applied to files:

  • packages/core/src/types/config.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Implement plugins using the `Plugin` interface with hooks: `onInit`, `onStart`, `onShutdown`, `beforeToolCall`, `afterToolCall`, `onToolError`

Applied to files:

  • packages/core/src/types/config.ts
  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use Zod schemas with `defineTool` for input/output validation

Applied to files:

  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use `AppError` and `ErrorCode` from `mcp-apps-kit/core` for error handling

Applied to files:

  • packages/core/src/createApp.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/tests/**/*.{test,spec}.{ts,tsx} : Mirror test file structure to source: tests in `tests/` directory with `unit/`, `integration/`, and `contract/` subdirectories

Applied to files:

  • packages/core/tests/unit/versioning.test.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Remove unused variables or prefix with underscore (`_`)

Applied to files:

  • packages/core/tests/unit/versioning.test.ts
🧬 Code graph analysis (1)
packages/core/src/types/config.ts (3)
packages/core/src/types/tools.ts (1)
  • ToolDefs (332-332)
packages/core/src/types/ui.ts (1)
  • UIDefs (141-141)
packages/core/src/plugins/types.ts (1)
  • Plugin (163-275)
🪛 GitHub Actions: PR Check
packages/core/src/createApp.ts

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in packages/core/src/createApp.ts. Step: 'pnpm format' (prettier --check .) exited with code 1.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🔇 Additional comments (10)
packages/core/src/types/config.ts (1)

248-449: LGTM! Well-structured type definitions for multi-version support.

The new types are well-designed and thoroughly documented:

  • DeepPartialWithNull<T> enables fine-grained control over config inheritance with clear null vs undefined semantics
  • VersionSpecificConfig, VersionConfig, and VersionsConfig provide a clean API for multi-version apps
  • AppConfigInput union maintains backward compatibility while enabling versioning
  • Documentation includes helpful examples and clear explanations
packages/core/src/createApp.ts (7)

73-79: LGTM! Simple and effective type guard.

The isVersionsConfig type guard correctly checks for the versions property to distinguish multi-version configs from single-version configs.


81-324: LGTM! Comprehensive validation with proper type narrowing.

The validation functions are well-structured:

  • validateVersionKey enforces the /^v\d+$/ pattern
  • validateVersionConfig validates all required fields and optional overrides
  • validateGlobalConfig properly handles null values for deep merge semantics
  • validateConfig safely narrows from unknown and validates both single and multi-version configs
  • All use AppError and ErrorCode as per coding guidelines
  • Error messages are clear and helpful

As per coding guidelines, proper error handling with AppError and ErrorCode is used throughout.


326-448: LGTM! Correct deep merge implementation.

The deep merge logic correctly implements the documented semantics:

  • null explicitly disables/removes properties
  • undefined inherits from global config
  • Objects are recursively merged
  • Arrays and primitives are replaced (not merged)
  • Type safety is maintained with Record<string, unknown> and appropriate casting

The mergeVersionConfig function properly applies deep merge to nested config objects while handling primitive properties separately.


497-507: LGTM! Clean dispatching logic.

The updated createApp signature accepts AppConfigInput<T> and uses the isVersionsConfig type guard to dispatch to the appropriate implementation, maintaining backward compatibility while enabling multi-version support.


509-744: LGTM! Backward-compatible single-version implementation.

The createSingleVersionApp function maintains backward compatibility while supporting the new API:

  • Lazy OAuth JWKS initialization is idempotent and handles both server and serverless scenarios
  • Middleware chain follows Koa-style async/await pattern as per coding guidelines
  • Error handling uses AppError and ErrorCode as required
  • getVersion() and getVersions() return appropriate values for single-version apps

As per coding guidelines, Koa-style async/await middleware pattern is used.


746-1002: LGTM! Well-architected multi-version implementation.

The first part of createMultiVersionApp sets up the shared infrastructure:

  • Shared Express app for all versions with per-version sub-apps
  • OAuth JWKS clients cached by config hash for efficient reuse across versions
  • Per-version lazy OAuth initialization is idempotent and handles race conditions
  • Version-specific server instances properly mounted with version routes
  • Shared endpoints (/health and /.well-known/openai-apps-challenge) correctly added to shared app

1008-1214: LGTM! Comprehensive multi-version app orchestration.

The main multi-version app instance correctly:

  • Delegates all operations to version-specific apps
  • Starts all versions and the shared HTTP server
  • Handles shared endpoints (/health, /.well-known/openai-apps-challenge) in handleRequest() for serverless deployments
  • Applies middleware to all version apps via use()
  • Delegates event subscriptions to all versions
  • Implements once() with proper shared state to ensure handler fires only once across all versions
  • Provides getVersion() and getVersions() for programmatic access

The once() implementation (lines 1146-1189) is notably complex but correct—it properly handles race conditions and ensures cleanup even if the handler throws.

packages/core/src/index.ts (1)

61-62: LGTM! Proper type-only exports.

The new type exports (VersionSpecificConfig and DeepPartialWithNull) are correctly added to the public API surface using export type as per coding guidelines.

As per coding guidelines, export type is used for type-only exports.

packages/core/tests/unit/versioning.test.ts (1)

14-967: Excellent test coverage for multi-version functionality.

The test suite comprehensively covers:

  • Multi-version app creation and version retrieval
  • Version key validation and error handling
  • Config merging (global + version-specific) with deep merge semantics
  • Route isolation across versions
  • Backward compatibility with single-version apps
  • Per-version middleware behavior
  • Serverless handleRequest() scenarios including shared endpoints
  • Tool execution isolation across versions

Well-structured and follows testing guidelines by placing tests in the unit/ subdirectory.

As per coding guidelines, test file structure mirrors source with tests in tests/unit/ subdirectory.

@claude

claude Bot commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Code Review: API Versioning Implementation

Thank you for this substantial feature addition! This PR implements API versioning support for MCP AppsKit, allowing multiple API versions to be exposed from a single application. Here's my detailed review:


Strengths

1. Excellent Architecture & Design

  • Clean separation of concerns: Single-version and multi-version apps are handled elegantly with shared code paths
  • Backward compatibility: Existing single-version apps continue to work without changes - excellent design decision
  • Deep merge strategy: The deepMerge function with null-to-disable semantics is well-thought-out and powerful
  • Type safety: Strong TypeScript usage with proper type inference and DeepPartialWithNull<T> utility type

2. Comprehensive Documentation

  • README updates clearly explain versioning concepts with practical examples
  • Package-level documentation is thorough
  • Inline comments explain complex logic (e.g., OAuth initialization, route mounting)
  • Example app demonstrates real-world usage effectively

3. Robust Testing

  • Unit tests cover config validation, merging, route isolation, and backward compatibility
  • Tests verify version key validation and serverRoute conflicts
  • Good coverage of edge cases

4. Security & Best Practices

  • OAuth JWKS client reuse across versions with identical configs (line 820-825 in createApp.ts)
  • Version key validation prevents injection attacks (/^v\d+$/)
  • Proper error handling throughout
  • Idempotent OAuth initialization

🔍 Issues & Concerns

Critical Issues

1. Resource Leak in Multi-Version Apps ⚠️

// packages/core/src/createApp.ts:899
// Express strips the mount path, so routes registered at serverRoute on the version app

The comment ends mid-sentence, suggesting incomplete implementation. More importantly, I don't see cleanup logic for:

  • Event emitters when versions are modified
  • Plugin managers per version
  • Middleware chains per version

Recommendation: Implement a dispose() or stop() method for multi-version apps that cleans up all version-specific resources.

2. Debug Logger Singleton Issue 🐛

// packages/core/src/createApp.ts:788-791
// Note: Debug logger is configured once with global config (line 681-683).
// We don't reconfigure it per-version because it's a global singleton.

This is explicitly acknowledged but creates a limitation:

  • Version-specific debug configs are ignored
  • All versions share the same debug logger configuration
  • If v1 needs level: "error" and v2 needs level: "debug", this doesn't work

Recommendation: Either:

  1. Document this limitation clearly in the versioning docs
  2. Make the debug logger context-aware (accepts a version identifier)
  3. Consider per-version logger instances

3. Missing Health Check Version Information 📋

// packages/core/src/server/index.ts:250-252
expressApp.get("/health", (_req: Request, res: Response) => {
  res.json({ status: "ok", name: config.name, version: config.version });
});

For single-version apps, this works fine. But for multi-version apps, the shared health endpoint should return ALL available versions, not just one. According to the PR description, it should "return all available versions".

Expected behavior:

{
  "status": "ok",
  "name": "my-app",
  "versions": [
    { "key": "v1", "version": "1.0.0", "route": "/v1/mcp" },
    { "key": "v2", "version": "2.0.0", "route": "/v2/mcp" }
  ]
}

4. Incomplete Route Implementation 🔧
Line 899 in createApp.ts has a truncated comment that suggests incomplete implementation:

// Note: Express strips the mount path, so routes registered at serverRoute on the version app

This needs to be completed or removed.


Medium Priority Issues

5. Inconsistent OAuth Audience Handling

// packages/core/src/server/index.ts:139-145
const protectedResourceUrl = new URL(serverRoute, config.config.oauth.protectedResource);
const oauthConfigWithAudience = {
  ...config.config.oauth,
  audience: config.config.oauth.audience ?? protectedResourceUrl.href,
};

For multi-version apps with different OAuth configs per version, the audience should include the version route (/v1/mcp, /v2/mcp). Currently, all versions might share the same base audience, which could cause validation issues.

Recommendation: Ensure version-specific OAuth audiences are properly constructed when OAuth configs differ per version.

6. Potential Memory Issue with JWKS Client Caching

// packages/core/src/createApp.ts:804-806
const oauthConfigKey = normalizedVersionConfig.config?.oauth
  ? JSON.stringify(normalizedVersionConfig.config.oauth)
  : "no-oauth";

Using JSON.stringify for cache keys can be fragile:

  • Property ordering might differ, causing cache misses
  • Large OAuth configs create large cache keys
  • No cleanup mechanism for cached JWKS clients

Recommendation: Consider using a hash function (crypto.createHash) or implement proper cache key normalization.

7. Missing Validation for Conflicting Version Routes
Version keys must match /^v\d+$/, but there's no validation to prevent:

  • A version with config.serverRoute = "/v2/mcp" when v2 exists
  • Custom serverRoutes that conflict with other version routes
  • Routes that conflict with reserved endpoints (/health, /.well-known/*)

Recommendation: Add validation in validateVersionConfig to detect route conflicts.


Low Priority / Code Quality

8. Type Assertions Could Be Improved

// packages/core/src/createApp.ts:414-434
const mergedConfig: GlobalConfig = {
  oauth: deepMerge(...) as GlobalConfig["oauth"],
  cors: deepMerge(...) as GlobalConfig["cors"],
  // ...
};

Multiple as assertions suggest the type inference could be improved. Consider making deepMerge generic with better type constraints.

9. Test Coverage Gaps
Looking at versioning.test.ts, I notice:

  • No integration tests for actual HTTP requests to versioned endpoints
  • No tests for OAuth with different configs per version
  • No tests for plugin hooks in multi-version context
  • No tests for concurrent requests to different versions

Recommendation: Add integration tests that start a server and make actual HTTP calls.

10. Example Code Quality

// examples/minimal/src/index.ts:53
},
});

// =============================================================================
// V2: Enhanced greet tool (name + optional surname)
// =============================================================================

The example is well-structured, but could be enhanced:

  • Add error handling in the UI components
  • Show how to handle version-specific errors
  • Demonstrate middleware per version

📝 Minor Issues

  1. Typo in comment (line 788): "line 681-683" should be updated to reflect actual line numbers after refactoring
  2. Inconsistent formatting: Some sections use // === separators, others don't
  3. Missing JSDoc: Several exported functions lack JSDoc comments (e.g., mergeVersionConfig)
  4. Console.log in production code: The example uses console.log which should use the debug logger

🎯 Recommendations

Must Fix Before Merge:

  1. ✅ Complete the truncated comment at line 899
  2. ✅ Fix health endpoint to return all versions for multi-version apps
  3. ✅ Document the debug logger singleton limitation
  4. ✅ Add resource cleanup mechanism for multi-version apps

Should Fix:

  1. Add route conflict validation
  2. Improve OAuth audience handling for versioned apps
  3. Use hash-based cache keys instead of JSON.stringify
  4. Add integration tests

Nice to Have:

  1. Improve type inference to reduce as assertions
  2. Add more example scenarios (error handling, versioned middleware)
  3. Consider adding a migration guide for existing apps

🚀 Overall Assessment

This is a well-designed and implemented feature that adds significant value to the framework. The core architecture is sound, backward compatibility is maintained, and the documentation is comprehensive.

Score: 8/10

The main issues are:

  • Incomplete implementation details (truncated comment, health endpoint)
  • Debug logger limitation that affects usability
  • Missing resource cleanup
  • Some edge cases not fully handled

Once the critical issues are addressed, this will be an excellent addition to the framework.

Recommendation: Request Changes - The issues are fixable and not fundamental flaws.


💡 Additional Suggestions

  1. Consider adding a versioning migration tool: Help users upgrade from v1 to v2 by showing diffs
  2. Version deprecation strategy: Add metadata to mark versions as deprecated
  3. Version routing middleware: Allow custom version resolution (e.g., from headers)
  4. OpenAPI/Swagger generation: Auto-generate API docs per version

Great work overall! Looking forward to seeing this feature land. 🎉

- Reformatted conditional checks in validateGlobalConfig for better clarity.
- Updated mergeVersionConfig to enhance readability by using parentheses for nested expressions.
- Ensured consistent handling of null and undefined values in configuration merging logic.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Fix all issues with AI Agents 🤖
In @examples/minimal/src/index.ts:
- Around line 131-133: The v2 handler currently overrides the client config with
protocol: "openai" (the config object containing protocol: "openai"), which
conflicts with docs that expect MCP for both v1 and v2; remove that protocol
override from the v2 config so v2 uses the default MCP protocol (or if OpenAI
protocol support is intended, update routing/docs to reflect POST /v2/openai and
adjust the v2 endpoint to deliberately set protocol: "openai" and handle any
OpenAI-specific request shapes). Ensure the change targets the config object
where protocol is set for v2.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e613e09 and 013031e.

📒 Files selected for processing (1)
  • examples/minimal/src/index.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript with no any types - use unknown and narrow instead

Files:

  • examples/minimal/src/index.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Remove unused variables or prefix with underscore (_)

Files:

  • examples/minimal/src/index.ts
**/index.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Export public API only in index.ts files

Files:

  • examples/minimal/src/index.ts
{packages/core,examples}/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

{packages/core,examples}/**/*.ts: Always use defineTool and defineUI for type inference when defining tools and UI components
Use Koa-style async/await middleware pattern with await next() to chain middleware execution
Use AppError and ErrorCode from @mcp-apps-kit/core for error handling
Implement plugins using the Plugin interface with hooks: onInit, onStart, onShutdown, beforeToolCall, afterToolCall, onToolError
Use app.events.on() and app.events.once() for event subscription with event types like app:init, tool:call, app:start
Use Zod schemas with defineTool for input/output validation
Colocate UI definitions near tool definitions using defineUI with html property pointing to compiled UI assets

Files:

  • examples/minimal/src/index.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use export type for type-only exports

Files:

  • examples/minimal/src/index.ts
🧠 Learnings (3)
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Always use `defineTool` and `defineUI` for type inference when defining tools and UI components

Applied to files:

  • examples/minimal/src/index.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use Zod schemas with `defineTool` for input/output validation

Applied to files:

  • examples/minimal/src/index.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Colocate UI definitions near tool definitions using `defineUI` with `html` property pointing to compiled UI assets

Applied to files:

  • examples/minimal/src/index.ts
🧬 Code graph analysis (1)
examples/minimal/src/index.ts (6)
packages/core/src/index.ts (3)
  • defineTool (181-181)
  • createApp (181-181)
  • ClientToolsFromCore (46-46)
packages/core/src/createApp.ts (2)
  • defineTool (1217-1217)
  • createApp (497-507)
packages/ui-react-builder/src/define.ts (1)
  • defineReactUI (74-90)
packages/ui-react-builder/src/index.ts (1)
  • defineReactUI (87-87)
examples/minimal/src/ui/GreetingWidgetV1.tsx (1)
  • GreetingWidgetV1 (12-99)
examples/minimal/src/ui/GreetingWidgetV2.tsx (1)
  • GreetingWidgetV2 (12-116)
🔇 Additional comments (4)
examples/minimal/src/index.ts (4)

1-15: LGTM! Excellent adherence to coding guidelines.

The file structure, imports, and setup correctly follow the established patterns:

  • Uses defineTool and defineReactUI for type inference as per guidelines
  • Imports Zod for schema validation
  • Properly imports versioned UI components
  • Clear documentation of versioning feature

21-54: LGTM! V1 tool implementation is clean and follows best practices.

The V1 greet tool correctly:

  • Uses Zod schemas for input/output validation as per guidelines
  • Leverages defineTool for proper type inference
  • Includes UI component binding with defineReactUI
  • Returns structured output with the _text field for display purposes

60-97: LGTM! V2 tool demonstrates proper version evolution.

The V2 implementation correctly:

  • Extends V1 schema with optional surname field (Zod 4 compatible)
  • Computes fullName appropriately, handling the optional surname case
  • Maintains consistent structure with V1 for clarity
  • Uses a dedicated V2 UI component for version-specific behavior

154-168: LGTM! Type exports follow TypeScript best practices.

The type exports correctly:

  • Use export type for type-only exports as per coding guidelines
  • Create isolated type namespaces (V1 and V2) preventing cross-version type mixing
  • Export both tool types (AppTools*) and inferred schema types (GreetInput*, GreetOutput*)
  • Provide strong type safety for the UI components consuming these types

Comment on lines +131 to +133
config: {
protocol: "openai",
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's examine the actual code at the specified lines
cat -n examples/minimal/src/index.ts | sed -n '100,140p'

Repository: AndurilCode/mcp-apps-kit

Length of output: 1060


🏁 Script executed:

#!/bin/bash
# Search for documentation about protocol and versioning
rg -n -A 3 -B 3 'protocol' examples/minimal/src/index.ts | head -50

Repository: AndurilCode/mcp-apps-kit

Length of output: 343


🏁 Script executed:

#!/bin/bash
# Check if there are other examples in the repository
find examples -name '*.ts' -type f | head -20

Repository: AndurilCode/mcp-apps-kit

Length of output: 211


🏁 Script executed:

#!/bin/bash
# Look for any documentation about mixed protocol patterns
rg -i 'mixed.*protocol|protocol.*version' --type md -A 3

Repository: AndurilCode/mcp-apps-kit

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check the restaurant-finder example to see if it also uses mixed protocols
cat -n examples/restaurant-finder/src/index.ts | sed -n '1,50p'

Repository: AndurilCode/mcp-apps-kit

Length of output: 1705


🏁 Script executed:

#!/bin/bash
# Search for how createApp handles config merging and versioning
rg -n 'createApp' --type ts -A 10 | head -100

Repository: AndurilCode/mcp-apps-kit

Length of output: 6742


🏁 Script executed:

#!/bin/bash
# Look for README or documentation in examples directory
find examples -name 'README*' -o -name '*.md' | xargs cat 2>/dev/null

Repository: AndurilCode/mcp-apps-kit

Length of output: 4978


🏁 Script executed:

#!/bin/bash
# Check core implementation to understand how config overrides work
rg -n 'versions|protocol' packages/core/src --type ts -B 2 -A 2 | head -80

Repository: AndurilCode/mcp-apps-kit

Length of output: 5094


Remove the protocol: "openai" override from v2 or clarify the intended behavior.

The code overrides v2's protocol to "openai" (lines 131-133), but the README documentation and all provided examples show both v1 and v2 endpoints use MCP protocol (POST /v1/mcp and POST /v2/mcp). This inconsistency between the code configuration and documented endpoints should be resolved—either remove the protocol override to keep both versions on MCP, or update the documentation and routing if OpenAI protocol support is actually intended for v2.

🤖 Prompt for AI Agents
In @examples/minimal/src/index.ts around lines 131-133, The v2 handler currently
overrides the client config with protocol: "openai" (the config object
containing protocol: "openai"), which conflicts with docs that expect MCP for
both v1 and v2; remove that protocol override from the v2 config so v2 uses the
default MCP protocol (or if OpenAI protocol support is intended, update
routing/docs to reflect POST /v2/openai and adjust the v2 endpoint to
deliberately set protocol: "openai" and handle any OpenAI-specific request
shapes). Ensure the change targets the config object where protocol is set for
v2.

@claude

claude Bot commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Code Review: API Versioning Implementation

Summary

This PR implements API versioning support for mcp-apps-kit, allowing a single application to expose multiple API versions at different routes (e.g., /v1/mcp, /v2/mcp). The implementation is comprehensive, well-tested, and maintains backward compatibility with single-version apps.

✅ Strengths

1. Excellent Architecture & Design

  • Clean separation between single-version and multi-version app creation
  • Smart config merging with deep merge logic for nested objects
  • Proper use of null vs undefined semantics (null = disable, undefined = inherit)
  • Shared Express app across versions reduces resource overhead
  • Lazy JWKS client initialization for OAuth supports multi-version scenarios efficiently

2. Strong Type Safety

  • Comprehensive TypeScript types with proper inference
  • VersionsConfig, VersionConfig, and DeepPartialWithNull types are well-designed
  • Validation functions with clear error messages
  • Type-safe version key pattern enforcement (/^v\d+$/)

3. Robust Testing

  • Extensive test coverage (967 lines of tests!)
  • Tests cover: validation, config merging, route isolation, middleware, backward compatibility, serverless
  • Integration tests verify actual HTTP endpoints work correctly
  • Good edge case coverage (404s, invalid versions, etc.)

4. Backward Compatibility

  • Single-version apps continue to work without changes
  • getVersions() returns empty array for single-version apps
  • getVersion() returns undefined for single-version apps
  • Clear migration path for existing users

5. Documentation

  • Comprehensive README updates with examples
  • Updated minimal example demonstrates versioning in practice
  • Clear API documentation for new methods

⚠️ Issues & Concerns

1. Critical: OAuth Middleware Pattern (Security)

Location: packages/core/src/createApp.ts:615, 888

The OAuth middleware receives a lazy getter for the JWKS client. The middleware needs to handle the case where getJwksClient() returns null during a request. If OAuth initialization fails or is delayed, requests could be processed without proper authentication.

Recommendation: Add explicit null checks in the OAuth middleware before token verification, consider failing fast with a 503 Service Unavailable if OAuth is configured but JWKS client isn't initialized.

2. Potential Bug: Event Handling Race Condition

Location: packages/core/src/createApp.ts:1150-1192

The once() implementation for multi-version apps has potential race condition where multiple versions could emit the same event simultaneously, causing the handler to execute multiple times despite the once semantics.

Recommendation: Use atomic compare-and-swap pattern or add integration test that fires events from multiple versions concurrently.

3. Config Validation: Missing Edge Cases

Location: packages/core/src/createApp.ts:84-91, 153-156

Version key validation rejects /health as serverRoute but doesn't validate conflicts with /.well-known/openai-apps-challenge or potential version key mount point conflicts.

Recommendation: Add validation for reserved routes and conflicting version paths.

4. Performance: Deep Merge Complexity

Location: packages/core/src/createApp.ts:341-388

The deepMerge function lacks protection against circular references and has no depth limit, potentially causing stack overflow with deeply nested configs.

Recommendation: Add max depth check (e.g., depth > 10 throws error) or consider using a well-tested library like lodash.merge.

5. Code Smell: Type Assertions

Location: packages/core/src/createApp.ts:419-438

Multiple type assertions in config merging reduce type safety. Consider using generics to preserve types through deepMerge or use Zod for runtime validation with type inference.

6. Potential Memory Leak: Server Cleanup

Location: packages/core/src/createApp.ts:759-1215

Multi-version apps create multiple server instances, plugin managers, middleware chains, and event emitters. There's no explicit cleanup/shutdown method for the multi-version main app.

Recommendation: Add a shutdown() method to properly cleanup resources.

💡 Suggestions for Enhancement

  1. Version Negotiation: Consider adding content negotiation support via Accept headers
  2. Version Deprecation: Support deprecation warnings and sunset dates per version
  3. Health Check Improvements: Include per-version status and uptime information
  4. Version-Specific Rate Limiting: Allow different rate limits per version

🔍 Minor Issues

  1. Debug Logger: Multi-version apps configure debug logger once globally (line 772-774) but version-specific debug configs are ignored. Document this limitation.
  2. Missing Export: VersionsConfig and VersionConfig types should be explicitly exported in packages/core/src/index.ts.
  3. Example Code: Consider adding vite config examples for multi-UI builds in the minimal example.

📊 Test Coverage Suggestions

  • Test OAuth with different configs per version
  • Test plugin execution order (global + version-specific)
  • Test concurrent version initialization
  • Test middleware execution isolation between versions

🎯 Overall Assessment

Quality: ⭐⭐⭐⭐½ (4.5/5)

This is a well-designed, thoroughly tested feature implementation. The architecture is sound, backward compatibility is maintained, and the documentation is comprehensive.

Main Concerns:

  1. OAuth lazy initialization safety (needs verification)
  2. Event handler race condition in once()
  3. Deep merge edge cases and performance
  4. Missing shutdown/cleanup for multi-version apps

Recommendation:Approve with minor changes

The issues identified are mostly edge cases and can be addressed incrementally. The core implementation is solid and ready for merge after:

  1. Adding null checks in OAuth middleware for lazy client getter
  2. Adding circular reference protection to deepMerge
  3. Documenting the debug logger limitation for multi-version apps
  4. (Optional but recommended) Adding a shutdown() method

Great work! This is a valuable addition to the framework. 🚀

…y handling

- Updated deepMerge function to build the result object without null properties.
- Replaced direct property deletion with Reflect.deleteProperty to comply with ESLint rules.
- Enhanced comments for clarity on the merging process and runtime validation in versioning tests.
@gabrypavanello
gabrypavanello merged commit 9aaf706 into main Jan 5, 2026
3 of 4 checks passed
@gabrypavanello
gabrypavanello deleted the versioning branch January 5, 2026 17:47
@claude

claude Bot commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Code Review: API Versioning Implementation

This PR introduces a comprehensive API versioning feature to mcp-apps-kit, enabling multiple API versions from a single application. Overall, this is a well-designed and thoroughly implemented feature with excellent test coverage.


✅ Strengths

1. Excellent Architecture & Design

  • Clean abstraction: The versioning implementation cleanly separates single-version and multi-version apps while maintaining backward compatibility
  • Type safety: Strong TypeScript typing throughout with proper generic constraints
  • Deep merge semantics: The null/undefined/value semantics for config merging is well-designed and intuitive
  • Shared infrastructure: Smart reuse of Express app and OAuth JWKS clients across versions reduces memory footprint

2. Outstanding Test Coverage

  • 967 lines of tests covering edge cases, config merging, route isolation, middleware, and backward compatibility
  • Tests use real MCP client connections, validating end-to-end behavior
  • Integration tests verify actual HTTP behavior, not just unit logic

3. Documentation Quality

  • Comprehensive updates to README and package docs
  • Clear examples in both root README and minimal example
  • Good inline code comments explaining complex logic

4. Backward Compatibility

  • Single-version apps continue to work unchanged
  • getVersions() and getVersion() gracefully return empty/undefined for single-version apps

🔧 Issues & Recommendations

1. Critical: OAuth Lazy Initialization Race Condition ⚠️

Location: packages/core/src/createApp.ts:825-889

Problem: If the initialization promise fails and is deleted from the map (line 882), a concurrent call could get undefined from jwksClients.get() and return without setting versionJwksClient, leaving it null.

Recommendation: Add error handling to ensure versionJwksClient is set or throw an error.


2. Type Safety: Unsafe Type Assertions in mergeVersionConfig

Location: packages/core/src/createApp.ts:429-444

Issue: Multiple as type assertions on deepMerge results bypass type checking. Runtime type errors if deepMerge returns unexpected types won't be caught.

Recommendation: Add runtime validation or make deepMerge generic with proper constraints.


3. Documentation: Missing Migration Guide

Issue: No explicit migration guide for users updating from older versions.

Recommendation: Add a Migration section to the README covering:

  • How to migrate single-version apps to multi-version
  • When to use versioning vs. not
  • Performance considerations for multi-version apps

4. Testing: Missing OAuth Error Scenarios

Issue: Tests don't cover OAuth initialization failures in multi-version apps.

Recommendation: Add test cases for:

  • Invalid authorization server URL
  • JWKS discovery timeout/failure
  • Concurrent version initialization with OAuth

5. Type Safety: mainApp.tools Type Inference

Location: packages/core/src/createApp.ts:1022

Issue: This uses tools from the first version for type inference, which may not match other versions tools.

Recommendation: Document this limitation clearly or consider a union type approach.


📊 Summary

Category Rating Notes
Architecture ⭐⭐⭐⭐⭐ Excellent design with clean separation of concerns
Code Quality ⭐⭐⭐⭐ High quality, some type safety improvements needed
Test Coverage ⭐⭐⭐⭐⭐ Outstanding - 967 lines of comprehensive tests
Documentation ⭐⭐⭐⭐ Very good, would benefit from migration guide
Security ⭐⭐⭐⭐ OAuth implementation solid, minor race condition
Performance ⭐⭐⭐⭐⭐ Smart resource sharing, minimal overhead
Backward Compat ⭐⭐⭐⭐⭐ Perfect - single-version apps unchanged

✅ Approval Recommendation

LGTM with minor fixes recommended

This is a high-quality implementation that adds significant value to the framework. The critical OAuth race condition should be addressed before merge, but overall this is excellent work.

Pre-merge Checklist:

  • Fix OAuth lazy initialization race condition (Critical)
  • Improve type safety in mergeVersionConfig (Recommended)
  • Add OAuth failure test cases (Recommended)
  • Clarify serverRoute behavior in multi-version mode (Documentation)

Great job on this feature! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant