Skip to content

[1c] MCP entrypoint + identity resolution#26

Merged
andrmaz merged 3 commits into
developfrom
cursor/mcp-auth-identity-fbab
Jun 20, 2026
Merged

[1c] MCP entrypoint + identity resolution#26
andrmaz merged 3 commits into
developfrom
cursor/mcp-auth-identity-fbab

Conversation

@andrmaz

@andrmaz andrmaz commented Jun 20, 2026

Copy link
Copy Markdown
Owner

What was built

Stand up the authenticated /mcp entrypoint. Validated JWT tokens are accepted, identity is resolved from the token + DB, and the response includes org + department scope. Unauthorized and invalid tokens are rejected with structured 401 errors.

Changes

New: IdentityService (apps/api/src/mcp/identity.service.ts)

Resolves the authenticated user's identity scope. Reads organizationId directly from the validated JWT claim and looks up the user's Primary Department from the UserDepartment join table. Returns null for departmentId when no primary department is configured.

Updated: MCPController

  • Protected with JwtAuthGuard — unauthenticated requests never reach the handler
  • Extracts userId and organizationId from req.user (set by Passport JWT strategy)
  • Delegates scope resolution to IdentityService
  • Response shape now includes scope: { organizationId, departmentId }

Updated: MCPModule

Imports AuthModule (provides JwtStrategy) and PrismaModule; provides IdentityService.

Updated: db-client.mock.ts

Added userDepartment.findFirst mock for test isolation.

Tests

  • mcp.controller.spec.ts — unit tests for the auth-aware controller (17 tests)
  • mcp.module.spec.ts — module compilation and provider wiring (5 tests)
  • mcp.integration.spec.ts — HTTP-level integration tests using supertest (9 tests):
    • Authenticated happy path: valid JWT → 201 with resolved org + dept scope
    • Null departmentId when user has no primary department
    • organizationId sourced from JWT, not request body
    • 401 for missing Authorization header
    • 401 for invalid/malformed token
    • 401 for wrong-secret token
    • 401 for expired token
    • 401 for missing Bearer prefix
    • IdentityService not called when auth fails
  • app.module.spec.ts — updated for new handleMCP signature

Acceptance criteria

  • An authenticated MCP client can call /mcp and receive a valid stub response
  • The response payload includes resolved organization and department scope
  • Requests with missing, invalid, or expired tokens are rejected with a clear error
  • Integration test covers the authenticated happy path and the rejection path

Test results

Test Suites: 7 passed, 7 total
Tests:       59 passed, 59 total
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added JWT authentication requirement to the API endpoint.
    • Extended endpoint response to include scope information with organization and department details.
  • Tests

    • Enhanced test coverage for authentication and authorization scenarios.
    • Added integration tests for endpoint security.
  • Chores

    • Added testing dependencies to support improved test coverage.

- Protect POST /mcp with JwtAuthGuard; missing/invalid/expired tokens
  return 401 with structured error
- Add IdentityService that resolves the authenticated user's primary
  Department from the UserDepartment join table
- MCPController now extracts userId and organizationId from the validated
  JWT (req.user), delegates scope resolution to IdentityService, and
  includes { scope: { organizationId, departmentId } } in every response
- Update MCPModule to import AuthModule (JwtStrategy) and PrismaModule
- Add supertest + @types/supertest for integration testing
- Rewrite mcp.controller.spec.ts for the auth-aware controller API
- Update mcp.module.spec.ts with new module dependencies and providers
- Add mcp.integration.spec.ts covering authenticated happy path and all
  rejection paths (missing token, invalid token, expired token, wrong secret)
- Update app.module.spec.ts and db-client.mock.ts for the new shape

Co-authored-by: Andrea Mazzucchelli <andrmaz@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8cfef84-68ab-465d-9380-352cbfb58c7e

📥 Commits

Reviewing files that changed from the base of the PR and between 4cbee76 and cb10d57.

📒 Files selected for processing (5)
  • apps/api/src/app.module.spec.ts
  • apps/api/src/mcp/identity.service.spec.ts
  • apps/api/src/mcp/identity.service.ts
  • apps/api/src/mcp/mcp.integration.spec.ts
  • apps/api/src/mcp/mcp.module.spec.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/api/src/app.module.spec.ts
  • apps/api/src/mcp/identity.service.ts
  • apps/api/src/mcp/mcp.module.spec.ts
  • apps/api/src/mcp/mcp.integration.spec.ts

📝 Walkthrough

Walkthrough

Adds IdentityService that resolves a user's primary department from Prisma, guards POST /mcp with JwtAuthGuard, and returns a scope field (organizationId, departmentId) in the response. MCPModule is updated to import AuthModule and PrismaModule. Unit, module, and integration tests are added or updated throughout.

Changes

JWT Auth and Identity Scope for MCP Endpoint

Layer / File(s) Summary
IdentityService and MCPController types/contracts
apps/api/src/mcp/identity.service.ts, apps/api/src/mcp/mcp.controller.ts
ResolvedScope interface and IdentityService.resolveScope are introduced. MCPController gains MCPQueryBody, MCPResponse (with scope), and RequestWithJwtUser local types.
MCPController JWT guard and scope resolution
apps/api/src/mcp/mcp.controller.ts
handleMCP is decorated with @UseGuards(JwtAuthGuard), reads req.user, calls identityService.resolveScope, and returns scope alongside existing stubbed context, policy, and answer.
MCPModule dependency wiring
apps/api/src/mcp/mcp.module.ts
AuthModule and PrismaModule added to imports; IdentityService added to providers.
IdentityService unit tests
apps/api/src/mcp/identity.service.spec.ts
New Jest suite verifies Prisma query shape, correct departmentId/null returns, call-count isolation, promise type, empty userId safety, and error propagation.
MCPController unit tests and db-client mock update
apps/api/src/__mocks__/db-client.mock.ts, apps/api/src/mcp/mcp.controller.spec.ts
mockPrismaClient extended with userDepartment.findFirst. Controller tests rewritten to mock IdentityService, validate resolveScope call arguments and counts, and assert scope propagation including nullable departmentId.
MCPModule spec with IdentityService assertion
apps/api/src/mcp/mcp.module.spec.ts
Stubs GoogleStrategy and PrismaService, sets JWT_SECRET, and adds a test that IdentityService is provided by the module.
MCP integration tests and app.module spec update
apps/api/package.json, apps/api/src/app.module.spec.ts, apps/api/src/mcp/mcp.integration.spec.ts
supertest dev dependency added. Integration suite boots the full NestJS app and covers the authenticated happy path (scope with resolved departmentId and null fallback) plus five 401 rejection scenarios. app.module.spec updated to pass a mock req.user to handleMCP.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant JwtAuthGuard
  participant MCPController
  participant IdentityService
  participant PrismaService

  Client->>JwtAuthGuard: POST /mcp { query } + Bearer token
  JwtAuthGuard-->>MCPController: req.user (AuthenticatedUser)
  MCPController->>IdentityService: resolveScope(userId, organizationId)
  IdentityService->>PrismaService: userDepartment.findFirst({ userId, isPrimary: true, department.organizationId })
  PrismaService-->>IdentityService: { departmentId } or null
  IdentityService-->>MCPController: { organizationId, departmentId | null }
  MCPController-->>Client: 201 { scope, context, policy, answer }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • [1c] MCP entrypoint + identity resolution #7: This PR directly implements the /mcp endpoint with JwtAuthGuard, IdentityService for scope resolution, and integration tests for both authenticated and rejected request scenarios described in that issue.

Possibly related PRs

  • andrmaz/cortex#21: This PR updates mcp.controller.spec.ts, mcp.module.spec.ts, and app.module.spec.ts to reflect the new auth-driven scope and IdentityService.resolveScope behavior introduced on top of that PR's MCP unit tests.
  • andrmaz/cortex#23: This PR consumes JwtAuthGuard and JwtStrategy in mcp.controller.ts and expects req.user populated by the JWT infrastructure established in that PR.

Suggested labels

enhancement

🐇 A rabbit hops through auth's gate,
JWT tokens seal my fate.
resolveScope finds my department true,
departmentId returned—or null will do!
The MCP endpoint now stands guard,
No unauthorized query shall pass hard. 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[1c] MCP entrypoint + identity resolution' accurately summarizes the main changes: implementing the MCP endpoint with JWT authentication and identity resolution via a new IdentityService.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/mcp-auth-identity-fbab

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cursor cursor Bot mentioned this pull request Jun 20, 2026
4 tasks
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

✅ Unit tests committed locally. Commit: 47eb396674fc61aef5399dba264e48f7a4a724cf

@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: 4

🧹 Nitpick comments (2)
apps/api/src/mcp/mcp.controller.ts (1)

8-10: 💤 Low value

Consider adding request body validation.

MCPQueryBody is a plain interface without validation decorators. If a request arrives with a missing or non-string query, it will pass through unvalidated. Consider converting to a class with class-validator decorators and applying ValidationPipe:

♻️ Optional enhancement
import { IsString, IsNotEmpty } from "class-validator";

export class MCPQueryBody {
  `@IsString`()
  `@IsNotEmpty`()
  query: string;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/mcp/mcp.controller.ts` around lines 8 - 10, The MCPQueryBody
interface lacks request body validation, allowing invalid requests to pass
through without validation errors. Convert MCPQueryBody from a plain interface
to a class and add class-validator decorators to the query property by importing
IsString and IsNotEmpty from class-validator, then applying these decorators
above the query field definition to ensure the field is a non-empty string at
runtime.
apps/api/src/mcp/mcp.integration.spec.ts (1)

141-199: ⚡ Quick win

Assert the structured 401 response body, not only the status code.

These tests validate rejection status, but they don’t lock the structured error payload contract described in this PR. Add body-shape assertions for at least one or two rejection paths.

Example assertion upgrade
     it("returns 401 when Authorization header is missing", async () => {
-      await request(app.getHttpServer())
+      const response = await request(app.getHttpServer())
         .post("/mcp")
         .send({ query: "test" })
         .expect(401);
+
+      expect(response.body).toMatchObject({
+        statusCode: 401,
+      });
+      expect(typeof response.body.message).toBe("string");
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/mcp/mcp.integration.spec.ts` around lines 141 - 199, The test
cases for the POST /mcp endpoint authorization failures (such as "returns 401
when Authorization header is missing", "returns 401 when Bearer token value is
invalid", "returns 401 when token is signed with wrong secret", "returns 401
when token is expired", and "returns 401 for a malformed Authorization header
(no Bearer prefix)") currently only validate the HTTP status code. Add
assertions to verify the structure and content of the 401 response body for at
least one or two of these test cases. This should include validating that
expected error fields (such as error message, error code, or status) are present
in the JSON response to lock in the structured error payload contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/mcp/identity.service.ts`:
- Around line 25-27: The findFirst query on this.prisma.userDepartment does not
include an organization-level constraint in the where clause. First, retrieve
the user's organization ID by querying the user record, then add an additional
filter in the where clause to ensure the returned UserDepartment belongs to the
user's organization (you may need to adjust the field name based on the actual
UserDepartment schema structure, but the goal is to filter by the organization
ID alongside the existing userId and isPrimary filters).

In `@apps/api/src/mcp/mcp.integration.spec.ts`:
- Around line 43-62: In the beforeAll hook, save the original value of
process.env["JWT_SECRET"] before overwriting it with TEST_JWT_SECRET. Then in
the afterAll hook, restore the original value instead of unconditionally
deleting the environment variable. This ensures that if JWT_SECRET was already
set before the test suite ran, it will be restored to its previous state,
preventing cross-suite side effects.

In `@apps/api/src/mcp/mcp.module.spec.ts`:
- Around line 23-30: In the MCP module spec test file, the beforeAll and
afterAll hooks are mutating global process state without preserving the original
state. Save the original value of process.env["JWT_SECRET"] before the beforeAll
hook executes, then in the afterAll hook, restore it to its original value
instead of unconditionally deleting it. This prevents other tests from becoming
flaky if they relied on a pre-existing JWT_SECRET value.
- Around line 45-56: The test "should not throw when creating the module" uses
an incorrect assertion pattern with `.resolves.not.toThrow()`. The `toThrow()`
matcher is designed for functions, not for resolved Promise values, which will
cause a matcher error. Replace `.resolves.not.toThrow()` with
`.resolves.toBeDefined()` to properly assert that the Promise resolves
successfully to a defined module object. Apply the same fix to similar test
patterns in apps/api/src/app.module.spec.ts.

---

Nitpick comments:
In `@apps/api/src/mcp/mcp.controller.ts`:
- Around line 8-10: The MCPQueryBody interface lacks request body validation,
allowing invalid requests to pass through without validation errors. Convert
MCPQueryBody from a plain interface to a class and add class-validator
decorators to the query property by importing IsString and IsNotEmpty from
class-validator, then applying these decorators above the query field definition
to ensure the field is a non-empty string at runtime.

In `@apps/api/src/mcp/mcp.integration.spec.ts`:
- Around line 141-199: The test cases for the POST /mcp endpoint authorization
failures (such as "returns 401 when Authorization header is missing", "returns
401 when Bearer token value is invalid", "returns 401 when token is signed with
wrong secret", "returns 401 when token is expired", and "returns 401 for a
malformed Authorization header (no Bearer prefix)") currently only validate the
HTTP status code. Add assertions to verify the structure and content of the 401
response body for at least one or two of these test cases. This should include
validating that expected error fields (such as error message, error code, or
status) are present in the JSON response to lock in the structured error payload
contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b1849fe-8eea-485f-b87a-5631b05d6648

📥 Commits

Reviewing files that changed from the base of the PR and between eedf3c0 and 4cbee76.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • apps/api/package.json
  • apps/api/src/__mocks__/db-client.mock.ts
  • apps/api/src/app.module.spec.ts
  • apps/api/src/mcp/identity.service.ts
  • apps/api/src/mcp/mcp.controller.spec.ts
  • apps/api/src/mcp/mcp.controller.ts
  • apps/api/src/mcp/mcp.integration.spec.ts
  • apps/api/src/mcp/mcp.module.spec.ts
  • apps/api/src/mcp/mcp.module.ts

Comment thread apps/api/src/mcp/identity.service.ts
Comment thread apps/api/src/mcp/mcp.integration.spec.ts
Comment thread apps/api/src/mcp/mcp.module.spec.ts
Comment thread apps/api/src/mcp/mcp.module.spec.ts
@andrmaz

andrmaz commented Jun 20, 2026

Copy link
Copy Markdown
Owner Author

@cursoragent Fix and resolve all the conversations

@cursor

cursor Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

All five CodeRabbit conversations have been resolved in a single commit:

  • Cross-org isolation (identity.service.ts): Added department: { organizationId } to the findFirst where clause so a user can never accidentally resolve a department that belongs to a different organization.

  • JWT_SECRET env mutation (mcp.integration.spec.ts, mcp.module.spec.ts): Both suites now save the pre-existing JWT_SECRET value in beforeAll and restore it in afterAll instead of unconditionally deleting it, preventing cross-suite side effects.

  • .resolves.not.toThrow() misuse (mcp.module.spec.ts, app.module.spec.ts): Replaced with .resolves.toBeDefined()toThrow is for functions, not resolved promise values; toBeDefined() correctly asserts the module compiled successfully.

  • Mock type in identity.service.spec.ts: Removed the incorrect as unknown as jest.Mocked<PrismaService> cast from makeMockPrisma() so the inferred jest.fn() types are preserved; moved the as unknown as PrismaService cast to the service constructor call. This lets TypeScript see .mockResolvedValueOnce and .mockRejectedValueOnce correctly. Also updated the two exact toHaveBeenCalledWith assertions to include the new department: { organizationId } filter.

All 73 tests pass across 8 suites.

View PR

Open in Web Open in Cursor 

@andrmaz andrmaz marked this pull request as ready for review June 20, 2026 14:04
- identity.service.ts: add department.organizationId filter to the
  findFirst query to prevent cross-organization department access
- identity.service.spec.ts: update exact toHaveBeenCalledWith assertions
  to match the new where clause shape; fix makeMockPrisma cast so jest.fn()
  methods are accessible on the mock without Prisma type interference
- mcp.integration.spec.ts: save and restore JWT_SECRET around the suite
  instead of unconditionally deleting it
- mcp.module.spec.ts: save and restore JWT_SECRET; replace
  .resolves.not.toThrow() with .resolves.toBeDefined()
- app.module.spec.ts: replace .resolves.not.toThrow() with
  .resolves.toBeDefined()

Co-authored-by: Andrea Mazzucchelli <andrmaz@users.noreply.github.com>
@andrmaz andrmaz merged commit b84104e into develop Jun 20, 2026
8 checks passed
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.

2 participants