feat: embedded MCP server with user-scoped token authentication (#15383)#41980
feat: embedded MCP server with user-scoped token authentication (#15383)#41980salevine wants to merge 71 commits into
Conversation
…15383) Add an opt-in, same-instance Streamable HTTP MCP endpoint that acts as the calling Appsmith user. MCP clients authenticate with a user-scoped bearer token; the Node service forwards that token to existing /api/v1 endpoints so Spring Security reconstructs the real user and existing workspace/app/page ACLs authorize every operation. No privileged/internal credential is used. Server (CE, EE-overridable via *CE base + thin concrete subclass split): - UserMcpToken domain/repository/service + McpTokenController for create/list/ revoke of user-scoped tokens (SHA-256 pre-hash then bcrypt at rest, plaintext shown once, max 10 active tokens/user). - Bearer AuthenticationWebFilter (mcp_ prefix) reconstructs the token owner; invalid/revoked/disabled tokens return 401. - Migration076 creates the userMcpToken indexes (auto-index-creation is off). Node service (app/client/packages/mcp): - Streamable HTTP transport, loopback bind, /health endpoint, request body cap, per-request token revalidation, per-session token binding, and per-user + global session caps. - Tools: list_workspaces, list_applications, get_application_context, and import_application_artifact / import_partial_application_artifact (validated artifact upload through the existing import APIs). Client: - MCP token management UI in the user profile (create / copy-once / revoke). Deploy/CI: - Opt-in APPSMITH_MCP_ENABLED gate (default off) for supervisord autostart and the Caddy /mcp route; Dockerfile copy, mcp-build workflow, route health test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Whoops! Looks like you're using an outdated method of running the Cypress suite. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds an MCP server, secure user token lifecycle, User Profile token management, backend authentication, Docker runtime support, and CI workflows that build and package MCP artifacts. ChangesMCP app builder and server
Token lifecycle and UI
Deployment and CI
Estimated code review effort: 5 (Critical) | ~120 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Failed server tests
|
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (4)
app/client/packages/mcp/src/app.ts (1)
67-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
objectKeysfrom@appsmith/utilsinstead ofObject.keys.Static analysis flags this per the repo's internal lint rule for consistent object-key handling.
🤖 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 `@app/client/packages/mcp/src/app.ts` at line 67, Replace the Object.keys call in the artifact emptiness check with the repository’s objectKeys utility imported from `@appsmith/utils`, while preserving the existing condition and behavior.Source: Linters/SAST tools
app/client/src/pages/UserProfile/McpTokens.test.tsx (1)
9-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFull-module mock of
McpTokenApiskips coverage oflist()'s response normalization.Mocking
McpTokenApi.listdirectly (rather than mockingApi.get) means this suite never exercises the array/response-unwrapping logic inside the reallist()implementation — see the concern raised inMcpTokenApi.ts.Also applies to: 32-38
🤖 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 `@app/client/src/pages/UserProfile/McpTokens.test.tsx` around lines 9 - 16, Replace the full-module mock of McpTokenApi with a mock of the underlying Api.get request, while retaining mocks for create and revoke as needed, so tests invoke the real McpTokenApi.list implementation and cover its array/response-unwrapping normalization logic..github/workflows/mcp-build.yml (1)
42-60: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winConsider
persist-credentials: falseon checkout.zizmor flags all three checkout steps for credential persistence in the git config, which could be exfiltrated by any subsequent step/dependency script in this job.
🔒 Disable credential persistence
- name: Checkout the merged pull-request commit if: inputs.pr != 0 uses: actions/checkout@v4 with: fetch-tags: true ref: refs/pull/${{ inputs.pr }}/merge + persist-credentials: falseApply similarly to the other two checkout steps.
🤖 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 @.github/workflows/mcp-build.yml around lines 42 - 60, All three checkout steps persist GitHub credentials in the local Git config. Add persist-credentials: false to the with configuration of the checkout steps identified by “Checkout the merged pull-request commit,” “Checkout the specified branch,” and “Checkout the head commit.”Source: Linters/SAST tools
app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java (1)
196-199: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider setting a stateless authentication success handler on the MCP filter.
AuthenticationWebFilterdefaults toWebSessionServerAuthenticationSuccessHandler, which creates a WebSession on each successful MCP token authentication. For bearer-token (stateless) auth, this is unnecessary session overhead. Set a no-op orSavedRequestServerAuthenticationSuccessHandlerto keep MCP auth stateless.♻️ Proposed fix
mcpTokenAuthenticationWebFilter.setServerAuthenticationConverter(mcpTokenAuthenticationConverter); mcpTokenAuthenticationWebFilter.setAuthenticationFailureHandler(failureHandler); +mcpTokenAuthenticationWebFilter.setAuthenticationSuccessHandler( + new ServerAuthenticationSuccessHandler() { + `@Override` + public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) { + return webFilterExchange.getChain().filter(webFilterExchange.getExchange()); + } + });🤖 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 `@app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java` around lines 196 - 199, Configure a stateless authentication success handler on the AuthenticationWebFilter created in SecurityConfig for MCP token authentication, replacing the default WebSessionServerAuthenticationSuccessHandler; use an appropriate no-op or SavedRequestServerAuthenticationSuccessHandler while retaining the existing failure handler.
🤖 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 @.github/workflows/build-client-server.yml:
- Around line 118-128: Update the mcp-build job’s if condition to compare
needs.file-check.outputs.runId with the quoted string '0', matching the runId
comparisons used by the other jobs in this workflow.
In @.github/workflows/mcp-build.yml:
- Around line 84-91: Remove the duplicate “Lint” step running yarn lint from the
workflow, keeping only one lint invocation alongside the existing formatting
check.
In `@app/client/packages/mcp/build.js`:
- Around line 3-12: Update the esbuild configuration in the build script to
derive the target from only the major Node version, such as by splitting
process.versions.node before constructing the target string; alternatively use a
fixed major-only value like node20. Ensure the target passed in the
esbuild.build call is accepted by esbuild.
In `@app/client/packages/mcp/src/app.test.ts`:
- Around line 294-298: Update the mockResolvedValueOnce object in the “fails
safely when session token revalidation fails” test to Prettier’s multiline
object-literal format, preserving its existing username and isAnonymous values.
- Line 59: Insert a blank line immediately before the for loop iterating over
callIndex in app.test.ts, preserving the required
padding-line-between-statements ESLint formatting.
- Around line 69-83: Fix the Prettier formatting in the test around the
fullArtifact and partialArtifact assertions: add the required blank line before
the fullArtifact declaration and collapse the fullArtifact.text() await expect
assertion to one line, matching the project’s formatting rules.
In `@app/client/packages/mcp/src/app.ts`:
- Line 14: Fix the Prettier formatting violations in app.ts at the declarations
and code associated with MAX_ARTIFACT_BYTES and the flagged lines 33, 120, and
297; run Prettier on the file and verify the build formatting check passes.
- Around line 143-159: Update the request function to enforce a finite timeout
for every fetchFn call. Create an AbortController, schedule it to abort after
the configured timeout, pass its signal into the fetch options while preserving
any caller-provided signal behavior, and clear the timeout in a finally block so
completed requests do not retain timers.
- Around line 448-458: Add Origin/Host validation for the /mcp endpoint before
creating or handling the StreamableHTTPServerTransport in the surrounding
request handler. Reject requests whose Origin or Host is not an explicitly
allowed local/ configured value, using middleware or equivalent request checks
rather than transport defaults; ensure rejected requests do not create sessions
or reach MCP processing.
In `@app/client/packages/mcp/src/server.ts`:
- Around line 8-18: Update reportProcessFailure to terminate the MCP process
after recording the failure: retain the stderr message, then call
process.exit(1) rather than only setting process.exitCode. Keep the
uncaughtException and unhandledRejection handlers wired to this function, and
apply the repository’s Prettier formatting to the affected code.</codeેન
In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserMcpTokenServiceCEImpl.java`:
- Around line 112-128: Update extractTokenId in UserMcpTokenServiceCEImpl to
handle a null token before calling startsWith, returning null for null or
invalid credentials so McpTokenAuthenticationManager can fall back to
Mono.empty() instead of throwing.
---
Nitpick comments:
In @.github/workflows/mcp-build.yml:
- Around line 42-60: All three checkout steps persist GitHub credentials in the
local Git config. Add persist-credentials: false to the with configuration of
the checkout steps identified by “Checkout the merged pull-request commit,”
“Checkout the specified branch,” and “Checkout the head commit.”
In `@app/client/packages/mcp/src/app.ts`:
- Line 67: Replace the Object.keys call in the artifact emptiness check with the
repository’s objectKeys utility imported from `@appsmith/utils`, while preserving
the existing condition and behavior.
In `@app/client/src/pages/UserProfile/McpTokens.test.tsx`:
- Around line 9-16: Replace the full-module mock of McpTokenApi with a mock of
the underlying Api.get request, while retaining mocks for create and revoke as
needed, so tests invoke the real McpTokenApi.list implementation and cover its
array/response-unwrapping normalization logic.
In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java`:
- Around line 196-199: Configure a stateless authentication success handler on
the AuthenticationWebFilter created in SecurityConfig for MCP token
authentication, replacing the default
WebSessionServerAuthenticationSuccessHandler; use an appropriate no-op or
SavedRequestServerAuthenticationSuccessHandler while retaining the existing
failure handler.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3f3c434d-959f-49df-8c12-f94b0bd1a5e6
⛔ Files ignored due to path filters (1)
app/client/yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (56)
.github/workflows/ad-hoc-docker-image.yml.github/workflows/build-client-server-count.yml.github/workflows/build-client-server.yml.github/workflows/build-docker-image.yml.github/workflows/docs/test-build-docker-image.md.github/workflows/github-release.yml.github/workflows/mcp-build.yml.github/workflows/on-demand-build-docker-image-deploy-preview.yml.github/workflows/playwright-e2e.yml.github/workflows/pr-cypress.yml.github/workflows/test-build-docker-image.ymlDockerfileapp/client/packages/mcp/.env.exampleapp/client/packages/mcp/.eslintignoreapp/client/packages/mcp/build.jsapp/client/packages/mcp/build.shapp/client/packages/mcp/jest.config.cjsapp/client/packages/mcp/package.jsonapp/client/packages/mcp/src/app.test.tsapp/client/packages/mcp/src/app.tsapp/client/packages/mcp/src/server.tsapp/client/packages/mcp/start-server.shapp/client/packages/mcp/tsconfig.jsonapp/client/src/api/McpTokenApi.tsapp/client/src/ce/constants/messages.tsapp/client/src/pages/UserProfile/McpTokens.test.tsxapp/client/src/pages/UserProfile/McpTokens.tsxapp/client/src/pages/UserProfile/index.tsxapp/server/appsmith-server/src/main/java/com/appsmith/server/authentication/converters/McpTokenAuthenticationConverter.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/authentication/managers/McpTokenAuthenticationManager.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/authentication/tokens/McpTokenAuthentication.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/controllers/McpTokenController.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/McpTokenControllerCE.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/domains/UserMcpToken.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/UserMcpTokenCE.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/dtos/McpTokenResponseDTO.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration076AddUserMcpTokenIndexes.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/repositories/UserMcpTokenRepository.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/UserMcpTokenRepositoryCE.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/services/UserMcpTokenService.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/services/UserMcpTokenServiceImpl.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserMcpTokenServiceCE.javaapp/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserMcpTokenServiceCEImpl.javaapp/server/appsmith-server/src/test/java/com/appsmith/server/authentication/McpTokenAuthenticationWebFilterTest.javaapp/server/appsmith-server/src/test/java/com/appsmith/server/authentication/converters/McpTokenAuthenticationConverterTest.javaapp/server/appsmith-server/src/test/java/com/appsmith/server/authentication/managers/McpTokenAuthenticationManagerTest.javaapp/server/appsmith-server/src/test/java/com/appsmith/server/services/UserMcpTokenServiceImplTest.javacontributions/ServerSetup.mddeploy/docker/fs/opt/appsmith/caddy-reconfigure.mjsdeploy/docker/fs/opt/appsmith/entrypoint.shdeploy/docker/fs/opt/appsmith/healthcheck.shdeploy/docker/fs/opt/appsmith/run-mcp.shdeploy/docker/fs/opt/appsmith/templates/supervisord/application_process/mcp.confdeploy/docker/route-tests/common/mcp-health.hurlscripts/local_testing.sh
| - name: Lint | ||
| run: yarn lint | ||
|
|
||
| - name: Check formatting | ||
| run: yarn prettier | ||
|
|
||
| - name: Lint | ||
| run: yarn lint |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Duplicate yarn lint step.
Lint runs at Line 85 and again at Line 91, wasting CI time for no benefit.
🧹 Remove the duplicate step
- name: Check formatting
run: yarn prettier
- - name: Lint
- run: yarn lint
-
- name: Run unit tests📝 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.
| - name: Lint | |
| run: yarn lint | |
| - name: Check formatting | |
| run: yarn prettier | |
| - name: Lint | |
| run: yarn lint | |
| - name: Lint | |
| run: yarn lint | |
| - name: Check formatting | |
| run: yarn prettier |
🤖 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 @.github/workflows/mcp-build.yml around lines 84 - 91, Remove the duplicate
“Lint” step running yarn lint from the workflow, keeping only one lint
invocation alongside the existing formatting check.
| transport = new StreamableHTTPServerTransport({ | ||
| sessionIdGenerator: randomUUID, | ||
| onsessioninitialized: (id) => { | ||
| sessions.set(id, { | ||
| expiresAt: now() + sessionTtlMs, | ||
| token, | ||
| username, | ||
| transport: transport!, | ||
| }); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n '"`@modelcontextprotocol/sdk`"' app/client/packages/mcp/package.jsonRepository: appsmithorg/appsmith
Length of output: 1992
🌐 Web query:
@modelcontextprotocol/sdk 1.12.0 StreamableHTTPServerTransport enableDnsRebindingProtection allowedOrigins
💡 Result:
In the @modelcontextprotocol/sdk, StreamableHTTPServerTransport provides DNS rebinding protection to secure HTTP-based servers running on localhost [1][2][3]. The following configuration options control this feature: enableDnsRebindingProtection (boolean, default: false): Must be explicitly set to true to enable the protection mechanism [1][4][5]. allowedHosts (string[], optional): A list of allowed Host header values [1][4][5]. If not specified, host validation is disabled [1][5]. allowedOrigins (string[], optional): A list of allowed Origin header values [1][4][5]. If not specified, origin validation is disabled [1][5]. When enabled, the transport validates incoming request headers against the provided allowedHosts and allowedOrigins lists [4]. If the headers do not match, the request is rejected [4]. Security Note: DNS rebinding protection is disabled by default for backwards compatibility [1][2][6]. It is highly recommended to enable this protection when running MCP servers locally without authentication [2][3]. For easier implementation, users are encouraged to use createMcpExpressApp, which enables this protection by default when binding to localhost [2][3]. Servers using the stdio transport are not affected by this vulnerability [2]. Failure to configure these settings correctly on unauthenticated local servers may allow malicious websites to perform DNS rebinding attacks [2][7].
Citations:
- 1: https://ts.sdk.modelcontextprotocol.io/v2/api/@modelcontextprotocol/server/server/streamableHttp.html
- 2: GHSA-w48q-cv73-mx4w
- 3: https://ts.sdk.modelcontextprotocol.io/documents/server.html
- 4: https://github.com/modelcontextprotocol/typescript-sdk/blob/e74a358728991216391995e8daa5d0573614abc5/src/server/streamableHttp.ts
- 5: https://cdn.jsdelivr.net/npm/@modelcontextprotocol/sdk@1.20.2/dist/esm/server/streamableHttp.d.ts
- 6: While using Streamable HTTP transport, can't use async callback for tools modelcontextprotocol/typescript-sdk#1106
- 7: https://nvd.nist.gov/vuln/detail/CVE-2025-66414
🌐 Web query:
site:github.com modelcontextprotocol sdk enableDnsRebindingProtection allowedOrigins StreamableHTTPServerTransport changelog
💡 Result:
In the Model Context Protocol (MCP) SDKs (such as the TypeScript and Kotlin SDKs), enableDnsRebindingProtection, allowedHosts, and allowedOrigins are configuration options for the StreamableHTTPServerTransport to defend against DNS rebinding attacks [1][2][3][4]. When enableDnsRebindingProtection is set to true, the transport validates incoming HTTP requests to ensure they originate from trusted sources [1][5]. Specifically, if configured, the transport checks the Host and Origin headers of incoming requests against the allowedHosts and allowedOrigins lists, respectively [1][2][3]. If a header does not match an entry in the corresponding allow-list, the request is rejected, typically with a 403 Forbidden status [1][5]. Key details regarding these settings: - Purpose: They act as a security layer for servers, particularly those running on loopback interfaces, to prevent malicious websites from using DNS rebinding to send unauthorized requests to local MCP servers [2][3][5]. - Default behavior: For backwards compatibility, enableDnsRebindingProtection defaults to false in many implementations [1]. Operators are encouraged to enable it and explicitly define allowedHosts and allowedOrigins for production or remote-accessible deployments [2][3]. - Non-browser clients: When allowedOrigins is configured, the validator requires an Origin header to be present [2]. Consequently, non-browser clients (such as curl) must include an appropriate Origin header in their requests to bypass this check [2]. These options are standard features in the current versions of the MCP TypeScript SDK (e.g., within WebStandardStreamableHTTPServerTransport and related classes) and the Kotlin SDK [6][4][7][8]. There is no specific single "changelog" entry for these settings as they were introduced as part of the transport security hardening effort within the SDKs [3][5].
Citations:
- 1: https://github.com/modelcontextprotocol/typescript-sdk/blob/e74a358728991216391995e8daa5d0573614abc5/src/server/streamableHttp.ts
- 2: https://github.com/jeanibarz/knowledge-base-mcp-server/blob/main/docs/rfcs/008-remote-transport.md
- 3: Harden HTTP transport: enable DNS-rebinding protection and default to loopback bind brave/brave-search-mcp-server#314
- 4: https://github.com/modelcontextprotocol/kotlin-sdk/releases
- 5: fix: enable DNS rebinding protection in HTTP transport dynatrace-oss/dynatrace-mcp#540
- 6: https://github.com/modelcontextprotocol/kotlin-sdk/blob/main/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt
- 7: https://github.com/modelcontextprotocol/typescript-sdk/blob/95d28cbae98e57df1b987b656a555777de976545/packages/server/src/server/streamableHttp.ts
- 8: https://github.com/modelcontextprotocol/typescript-sdk/blob/2c0c481cb9dbfd15c8613f765c940a5f5bace94d/packages/server/src/server/streamableHttp.ts
🌐 Web query:
npm @modelcontextprotocol/sdk StreamableHTTPServerTransport allowedOrigins enableDnsRebindingProtection
💡 Result:
In the Model Context Protocol (MCP) TypeScript SDK, allowedOrigins and enableDnsRebindingProtection are configuration options for StreamableHTTPServerTransport (and its underlying WebStandardStreamableHTTPServerTransport) designed to provide security against DNS rebinding and unauthorized cross-origin requests [1][2][3]. However, please note that these specific properties have been marked as deprecated in recent versions of the SDK [1]. The official guidance is to use external middleware for host and origin validation instead of relying on these built-in transport options [4][1]. Configuration Details: - allowedOrigins: A list of strings representing allowed Origin header values [1][2]. If configured, incoming requests with a present but disallowed Origin header are rejected with a 403 error [4][1]. If left unspecified, origin validation is disabled [1][2]. - enableDnsRebindingProtection: A boolean flag that, when set to true, enables DNS rebinding protection [1][2][3]. It requires allowedHosts and/or allowedOrigins to be configured to function effectively [1][2]. It defaults to false for backwards compatibility [1][2][3]. Security Context: Running an HTTP-based MCP server on localhost without proper authentication can expose it to vulnerabilities [5]. If you are running an MCP server locally, it is recommended to enable these protections (or implement equivalent middleware) and explicitly configure allowedHosts (e.g., to include ['127.0.0.1', 'localhost']) and/or allowedOrigins to secure your server against malicious access attempts [6]. For implementation, modern applications should prefer mounting framework-specific middleware or using the framework-agnostic helpers (e.g., originValidationResponse) provided by @modelcontextprotocol/server to guard your endpoints before requests reach the transport handler [4].
Citations:
- 1: https://github.com/modelcontextprotocol/typescript-sdk/blob/2c0c481cb9dbfd15c8613f765c940a5f5bace94d/packages/server/src/server/streamableHttp.ts
- 2: https://cdn.jsdelivr.net/npm/@modelcontextprotocol/sdk@1.20.2/dist/esm/server/streamableHttp.d.ts
- 3: https://github.com/modelcontextprotocol/typescript-sdk/blob/e74a358728991216391995e8daa5d0573614abc5/src/server/streamableHttp.ts
- 4: https://github.com/modelcontextprotocol/TypeScript-sdk/blob/main/docs/server.md
- 5: https://nvd.nist.gov/vuln/detail/CVE-2025-66414
- 6: While using Streamable HTTP transport, can't use async callback for tools modelcontextprotocol/typescript-sdk#1106
Add Origin/Host validation on /mcp. The bearer token reduces exposure, but this localhost HTTP endpoint still needs a rebinding guard; use middleware or equivalent checks instead of relying on transport defaults.
🤖 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 `@app/client/packages/mcp/src/app.ts` around lines 448 - 458, Add Origin/Host
validation for the /mcp endpoint before creating or handling the
StreamableHTTPServerTransport in the surrounding request handler. Reject
requests whose Origin or Host is not an explicitly allowed local/ configured
value, using middleware or equivalent request checks rather than transport
defaults; ensure rejected requests do not create sessions or reach MCP
processing.
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/29135322682. |
Add the high-level page-spec app-builder that compiles to a validated Appsmith import artifact, and harden the MCP surface following an adversarial red-team pass. App-builder (app/client/packages/mcp/src/builder): - Zod page/app/edit spec + best-effort placement, curated widget templates, 64-column auto-layout, spec -> import-artifact compiler with depth and total-widget caps, presets, and a capability catalog. - New tools: get_capabilities, list_presets, get_preset, validate_app_spec (dry-run), build_application, edit_page (append-only, best-effort placement). - Removed the raw artifact-import tools (an arbitrary-content injection surface) in favour of the validated compiler. Security hardening (from the red-team review): - Atomic session admission so concurrent initializes cannot bypass the per-user / global session caps (whole-instance DoS). - Stop crash-looping: guard fire-and-forget transport.close(), log instead of process.exit on unhandledRejection, supervisord startsecs=5. - Do not evict a session on token mismatch (targeted DoS via a leaked session id). - Bound inbound sockets (requestTimeout / headersTimeout / maxConnections). - Token expiry (90d) enforced at authentication; block minting new MCP tokens from an MCP-authenticated principal (no post-revocation persistence). Tests: 39 Node tests (compiler, placement, edit, tool wiring, concurrent session caps, mismatch-no-evict) and Java unit tests for token expiry and the auth marker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/29160630878. |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserMcpTokenServiceCEImpl.java (1)
38-47: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winExpired tokens still consume the active-token quota.
countByUserIdAndDeletedAtIsNullcounts non-deleted tokens regardless ofexpiresAt, so a user who accumulates expired (but un-revoked) tokens can be blocked from creating new ones atMAX_ACTIVE_TOKENS_PER_USERuntil they manually revoke. Consider excluding expired tokens from the count or purging them.🤖 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 `@app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserMcpTokenServiceCEImpl.java` around lines 38 - 47, Update the active-token quota check in UserMcpTokenServiceCEImpl.create to exclude tokens whose expiresAt is in the past, using an expiry-aware repository query or purging expired tokens before counting. Preserve the existing deleted-token filtering and MAX_ACTIVE_TOKENS_PER_USER limit for currently valid tokens.
🧹 Nitpick comments (1)
docs/plans/2026-07-11-mcp-app-builder-design.md (1)
82-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a language specifier to the fenced code block.
Markdownlint MD040 flags code blocks without a language. Use
textorplaintextfor the directory structure.📝 Proposed fix
-``` +```text src/builder/ schema.ts Zod schema: page spec + app spec + edit spec + placement templates.ts curated getDefaults per widget (text,input,select,button,image,table,container) layout.ts vertical auto-placement on the 64-col grid; placement resolution compile.ts pageSpec[] -> import artifact; edit merge into existing DSL; depth/total-widget caps presets.ts form, table-detail, card-grid, crud capabilities.ts machine-readable widget/preset catalog</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@docs/plans/2026-07-11-mcp-app-builder-design.mdaround lines 82 - 90, Update
the fenced directory-structure block in the design document to include a text or
plaintext language specifier, preserving its contents and formatting.</details> <!-- cr-comment:v1:ff970ee68c5d25f17169e84b --> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>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@app/client/packages/mcp/src/app.test.ts:
- Around line 297-304: Update the excluded-tool assertion in the test around
names so it verifies every listed tool is absent, rather than only rejecting the
case where all are present. Use expect.not.arrayContaining with the excluded
tool names or add individual not.toContain checks, while preserving the existing
tool-list validation.In
@app/client/packages/mcp/src/builder/compile.ts:
- Around line 285-301: The placement update after adding a node only grows the
targeted inner canvas, leaving its enclosing CONTAINER_WIDGET height stale for
inside edits. In the resolvePlacement inside-edit flow, after updating
placement.canvas.bottomRow, also update the parent container’s bottomRow to
encompass the child canvas’s new extent, preserving existing root-canvas sizing
behavior.In
@app/client/packages/mcp/src/builder/schema.ts:
- Around line 20-25: Update placementSchema to enforce mutual exclusivity
between its optional after and inside fields with a refinement, while continuing
to allow either field alone or neither field. Keep the existing non-empty string
validation and strict object behavior unchanged.In
@app/server/appsmith-server/src/test/java/com/appsmith/server/authentication/managers/McpTokenAuthenticationManagerTest.java:
- Around line 99-104: Remove the unconditional unknown-source rate-limit stub
from the shared manager() helper. Stub
RateLimitConstants.BUCKET_KEY_FOR_MCP_AUTHENTICATION with "unknown" only in
tests that invoke that lookup, or mark the shared stub lenient, while preserving
the existing manager construction.
Outside diff comments:
In
@app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserMcpTokenServiceCEImpl.java:
- Around line 38-47: Update the active-token quota check in
UserMcpTokenServiceCEImpl.create to exclude tokens whose expiresAt is in the
past, using an expiry-aware repository query or purging expired tokens before
counting. Preserve the existing deleted-token filtering and
MAX_ACTIVE_TOKENS_PER_USER limit for currently valid tokens.
Nitpick comments:
In@docs/plans/2026-07-11-mcp-app-builder-design.md:
- Around line 82-90: Update the fenced directory-structure block in the design
document to include a text or plaintext language specifier, preserving its
contents and formatting.</details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: Path: .coderabbit.yaml **Review profile**: CHILL **Plan**: Pro **Run ID**: `a1429438-95f0-4da8-aec5-f218de3fea14` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between f07f6bf587b22f2c50a023f716837e70d80bcf29 and b55ba0aecbf5bae02c3f98acdaff88861624b3a8. </details> <details> <summary>📒 Files selected for processing (31)</summary> * `.github/workflows/build-client-server.yml` * `.github/workflows/mcp-build.yml` * `app/client/packages/mcp/build.js` * `app/client/packages/mcp/src/app.test.ts` * `app/client/packages/mcp/src/app.ts` * `app/client/packages/mcp/src/builder/builder.test.ts` * `app/client/packages/mcp/src/builder/capabilities.ts` * `app/client/packages/mcp/src/builder/compile.ts` * `app/client/packages/mcp/src/builder/layout.ts` * `app/client/packages/mcp/src/builder/presets.ts` * `app/client/packages/mcp/src/builder/schema.ts` * `app/client/packages/mcp/src/builder/templates.ts` * `app/client/packages/mcp/src/server.ts` * `app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/converters/McpTokenAuthenticationConverter.java` * `app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/managers/McpTokenAuthenticationManager.java` * `app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/tokens/McpTokenAuthentication.java` * `app/server/appsmith-server/src/main/java/com/appsmith/server/constants/RateLimitConstants.java` * `app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/McpTokenControllerCE.java` * `app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/UserMcpTokenCE.java` * `app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/McpTokenResponseDTO.java` * `app/server/appsmith-server/src/main/java/com/appsmith/server/ratelimiting/RateLimitConfig.java` * `app/server/appsmith-server/src/main/java/com/appsmith/server/ratelimiting/ce/RateLimitServiceCE.java` * `app/server/appsmith-server/src/main/java/com/appsmith/server/ratelimiting/ce/RateLimitServiceCEImpl.java` * `app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserMcpTokenServiceImpl.java` * `app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserMcpTokenServiceCEImpl.java` * `app/server/appsmith-server/src/test/java/com/appsmith/server/authentication/McpTokenAuthenticationWebFilterTest.java` * `app/server/appsmith-server/src/test/java/com/appsmith/server/authentication/converters/McpTokenAuthenticationConverterTest.java` * `app/server/appsmith-server/src/test/java/com/appsmith/server/authentication/managers/McpTokenAuthenticationManagerTest.java` * `app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserMcpTokenServiceImplTest.java` * `deploy/docker/fs/opt/appsmith/templates/supervisord/application_process/mcp.conf` * `docs/plans/2026-07-11-mcp-app-builder-design.md` </details> <details> <summary>🚧 Files skipped from review as they are similar to previous changes (8)</summary> * deploy/docker/fs/opt/appsmith/templates/supervisord/application_process/mcp.conf * app/server/appsmith-server/src/test/java/com/appsmith/server/authentication/converters/McpTokenAuthenticationConverterTest.java * app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/UserMcpTokenCE.java * app/client/packages/mcp/build.js * app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/converters/McpTokenAuthenticationConverter.java * app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/McpTokenControllerCE.java * app/server/appsmith-server/src/test/java/com/appsmith/server/authentication/McpTokenAuthenticationWebFilterTest.java * .github/workflows/build-client-server.yml </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
Failed server tests
|
…on-reservation leak From adversarial + UX + architect reviews: - healthcheck.sh: only health-check the mcp supervisord program when it exists, so a container with MCP disabled (the default) is not reported unhealthy fleet-wide. - Surface token expiry in the profile UI (list rows + created-token modal) — the server already returned expiresAt; the client was discarding it, so agents would die silently at 90 days. Fail closed on a null expiry server-side. - Release the MCP session reservation at onsessioninitialized (registration) instead of only in finally, so a stalled initialize SSE stream cannot pin the reservation and saturate the session caps. - Exclude packages/mcp from the client tsconfig (it has its own tsc, like rts) and use as-unknown-as casts in McpTokenApi so the client check-types passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserMcpTokenServiceCEImpl.java (1)
38-65: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚖️ Poor tradeoffCount-then-save is a TOCTOU on the active-token cap.
countByUserIdAndDeletedAtIsNullfollowed bysaveisn't atomic, so concurrentcreaterequests for the same user can each pass the check and push the active count pastMAX_ACTIVE_TOKENS_PER_USER. Low blast radius (a soft safety cap), so fine to defer — flagging for awareness.🤖 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 `@app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserMcpTokenServiceCEImpl.java` around lines 38 - 65, Update UserMcpTokenServiceCEImpl.create so enforcing MAX_ACTIVE_TOKENS_PER_USER is atomic with token creation, preventing concurrent requests from passing the count check and exceeding the active-token cap. Replace the separate countByUserIdAndDeletedAtIsNull-then-save flow with the repository or transaction-level mechanism used for atomic enforcement, while preserving the existing error and response behavior.
🤖 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 `@deploy/docker/fs/opt/appsmith/healthcheck.sh`:
- Around line 3-8: Preserve health checks for supervisor-managed mongo and redis
while making MCP checks conditional. In the health-check logic around the
processes list and supervisorctl status invocation, revert to querying
unfiltered supervisor status so absent MCP remains naturally excluded, or
conditionally add mongo and redis alongside mcp if retaining filtering; ensure
the existing mongo and redis branches remain reachable.
- Line 5: Update the health-check branch that currently matches “server” to
match “backend”, aligning it with the backend entry in the processes list so the
HTTP endpoint probe runs in addition to the RUNNING check; alternatively,
support both names without changing the existing probe behavior.
---
Nitpick comments:
In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserMcpTokenServiceCEImpl.java`:
- Around line 38-65: Update UserMcpTokenServiceCEImpl.create so enforcing
MAX_ACTIVE_TOKENS_PER_USER is atomic with token creation, preventing concurrent
requests from passing the count check and exceeding the active-token cap.
Replace the separate countByUserIdAndDeletedAtIsNull-then-save flow with the
repository or transaction-level mechanism used for atomic enforcement, while
preserving the existing error and response behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 02f59e36-1a8a-4bd3-bbc4-72dc6e52768c
📒 Files selected for processing (8)
app/client/packages/mcp/src/app.tsapp/client/src/api/McpTokenApi.tsapp/client/src/ce/constants/messages.tsapp/client/src/pages/UserProfile/McpTokens.test.tsxapp/client/src/pages/UserProfile/McpTokens.tsxapp/client/tsconfig.jsonapp/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserMcpTokenServiceCEImpl.javadeploy/docker/fs/opt/appsmith/healthcheck.sh
✅ Files skipped from review due to trivial changes (1)
- app/client/tsconfig.json
🚧 Files skipped from review as they are similar to previous changes (5)
- app/client/src/ce/constants/messages.ts
- app/client/src/api/McpTokenApi.ts
- app/client/src/pages/UserProfile/McpTokens.test.tsx
- app/client/src/pages/UserProfile/McpTokens.tsx
- app/client/packages/mcp/src/app.ts
CI server-unit-tests failures: - CsrfTest: the MCP AuthenticationWebFilter, added at AUTHENTICATION order with a match-any matcher, collided with the form-login filter and broke POST /api/v1/login (No provider found for UsernamePasswordAuthenticationToken -> 500). Restrict the filter to only engage for 'Bearer mcp_' requests so it never touches other auth flows. - McpTokenAuthenticationManagerTest: make the shared rate-limit stub lenient (tests that reject non-MCP / rate-limited requests never reach it) and wrap the failure-path tryIncreaseCounter in Mono.defer so it is not eagerly evaluated on the success path (which NPE'd on the unstubbed mock). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Failed server tests
|
Failed server tests
|
- SecurityConfig: add the MCP AuthenticationWebFilter BEFORE the AUTHENTICATION order instead of AT it. Two AuthenticationWebFilters at the same order break form-login's manager wiring, causing POST /api/v1/login to 500 (CsrfTest failure). - McpTokens.test.tsx: assert the full monospace font-family stack that actually renders; toHaveStyle requires the exact value, not a prefix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Failed server tests
|
Root cause of the CsrfTest failure: McpTokenAuthenticationManager was the only ReactiveAuthenticationManager @component in the server, so Spring Boot auto-wired it as the global default authentication manager. Form-login (which sets no explicit manager) then used it and rejected UsernamePasswordAuthenticationToken with 'No provider found' -> POST /api/v1/login returned 500. Fix: drop @component from the manager and construct it directly in SecurityConfig for the MCP filter only, so the context has no global ReactiveAuthenticationManager bean and form-login resolves its default (UserDetailsService-based) manager as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…outId, route contract
First real end-to-end run against a live Appsmith server surfaced three
integration bugs the entire unit suite missed because the affected API calls
are mocked. A pure mock cannot catch a wrapper that encodes a wrong route.
F1 — getApplication hit a dead route (GET /api/v1/applications/{id} → 405).
ApplicationControllerCE has no @GetMapping("/{id}"), so getApplication threw on
every call. The branch gate (gateGitState) is fail-CLOSED, so this made it
REFUSE every mutation with git_state_unknown — the entire editing surface was
broken against the real server. Redefined the single getApplication wrapper to
source the `.application` sub-object from the working pages DTO
(GET /api/v1/pages?applicationId=...&mode=EDIT, already a 200). This auto-fixes
all five consumers; the fail-open (advisory) vs fail-closed (gate/commit/
publish) split is preserved by construction — no consumer edited. The wrapper
returns `.application` (not the whole DTO, which would drop gitApplicationMetadata
and silently disable the gate) and never passes raw git metadata to the agent —
read_git_status stays a host-only projection.
F2 — build_application now returns editorUrl/viewerUrl by sourcing page slugs
from the pages DTO (reuses applicationUrlsFromPages + the governed path's
existing getApplicationPages read; extra read only on the ungoverned branch).
F3 — layoutId is now surfaced (new getPage wrapper → layouts[0].id) in
build_application (default page) and read_pages (per page), so the authoring
tools are usable after a fresh build. read_pages' per-page fan-out is bounded to
a small concurrency pool so a many-page app degrades to a few waves, not an
N-wide burst of full-DSL reads.
F5 — new route-contract test (routeContract.test.ts): instruments the real
createAppsmithApi with a recording fetch, captures every wrapper's (verb, path),
and asserts each is served by a real Spring controller route (parsed from the
Java source). Catches the whole M9 bug class. It immediately found a SECOND
405: updateActionCollection used PUT against the PATCH-only /{id} route
(edit_js_object would have failed live) — fixed to PATCH, matching both the
server route and the existing request builder.
Hardening from the code council:
- The fail-closed gate now treats a 2xx with no application object as
unreadable → git_state_unknown (shared unreadableGitState), closing the last
residual F1 edge where a malformed 200 could read a git app as not-connected.
Verified: check-types clean, ESLint 0 errors, 796 unit tests green (3/3 runs),
and a live end-to-end harness run against the real server 16/16 (patch_widgets
applies, read_git_status → connected:false, build returns an editorUrl,
read_pages layoutId accepted by read_semantic_page).
Council: senior-architect + security-reviewer + qa-engineer + performance-engineer
→ APPROVE WITH RISKS (security clean APPROVE). Tracked follow-ups (non-blocking):
a pre-existing supertest socket-race flake in app.test.ts's M2 block; a
server-side lightweight layoutId route to drop the getPage over-fetch; and the
MCP-auth rate-limit shared-bucket hardening (F4, Java-side).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hPCEAQ998veVY29Mhac2s
The GitNexus code-intelligence index is a local, machine-specific artifact that should not be tracked. Ignore it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019hPCEAQ998veVY29Mhac2s
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/29608390077. |
|
Deploy-Preview-URL: https://ce-41980.dp.appsmith.com |
… confirms Field feedback: claude.ai declares the elicitation capability but users never see the approval dialog, and every failure surfaced as 'the user did not approve' — misleading the agent and making broken clients undiagnosable. Non-accept results now carry a reason (declined | cancelled | timeout | accepted_without_confirm | client_error) with per-reason error copy that never blames the user for a client-side failure, plus a sanitized client-reported detail string (control characters flattened, 300-char cap, untrusted-marked) on client errors. Approval semantics, attempt counters, session budget, and token lifecycle are unchanged; reason is advisory diagnostics only. Council-reviewed (architect, security, QA, DX): security re-verified the detail sanitization and approval posture; all findings resolved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
Milestone A of the approval-flow feedback work: - client_error outcomes refund the prompt charges and degrade the session to the documented relay posture, so a client that declares elicitation but cannot render it no longer dead-ends every destructive operation - an explicit accept now counts as approval even when the client UI omits the optional confirm boolean (bare accept/decline UIs); confirm=false still refuses, malformed values still classify as non-accept - prompt copy now frames accepting as approval (consent-frame condition from security review), centralized for all seven confirm tools - per-outcome telemetry (tool, outcome, client name/version) via an injectable logSink, default stderr - operator knobs: APPSMITH_MCP_DISABLE_ELICITATION forces the relay posture; APPSMITH_MCP_ELICITATION_TIMEOUT_MS overrides the wait (parsed in gates.ts, warn-and-clamp at a 10-minute ceiling) - README documents the reason vocabulary, degradation, and knobs, pinned by docs-drift tests Council-reviewed (architect, security, QA): all conditions applied and re-verified by security; strict-elicitation mode tracked as follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
The scalar sibling of the table query binding — 'show the temperature
from this API in a label' no longer requires a table workaround (the
field feedback that motivated this: a day-of-week readout was built as
a REST query + table + button because text had no query path).
- queryFieldRefSchema ({query, field?}) + compileQueryFieldBinding
emitting {{ Q.data?.field ?? "" }} — same closed-vocabulary and
charset discipline as every other emitter
- text source widens to selected-row | query-field; image gains source
(build + patch paths, mutual exclusion with literals)
- read_semantic_page reports the new shape (and now also reads back the
image prop, closing a pre-existing gap)
- responseFieldPath tightened to dot-separated identifiers (a trailing
or doubled dot compiled to a JS syntax error) per security review
- bindings guide teaches scalar readouts; the old table bullet is
requalified so the guide no longer contradicts itself
Council-reviewed (security, architect, QA): unanimous approve-with-risks;
all requested fixes and tests applied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
…er events Closes the lifecycle-event gap from the capability audit: onOptionChange, onSubmit, onCheckChange, onChange, and onDateSelected join the closed event vocabulary (EVENTS_BY_TYPE + enum), riding the existing generic trigger mechanism unchanged. Widget-type/event pairs verified against the canonical widget sources; cross-type misuse still rejects. Page-load execution needs no event: the server derives on-page-load actions from widget bindings at every layout save, so query-bound widgets already run on load — now stated in the tool copy. Council-reviewed (security APPROVE, architect APPROVE, QA approve-with- risks — closed-enum pin and select end-to-end test added per QA). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
'Show me the day of the week' is now one widget with zero queries:
text accepts a computed 'value' token from a closed vocabulary —
- { now: { format } }: the agent picks a NAMED preset (dayOfWeek, date,
dateShort, time, dateTime, isoDate, monthYear); the compiler owns the
moment format literal, so no free-form format string is ever emitted
- { count: { query, field? } }: row count of a query's response
- { concat: [...] }: literals (JSON-escaped at compile time — fuzz-
verified inert by security review across 78k hostile inputs),
selected-row refs, and query-field refs joined into one string
Build + patch paths with exactly-one-of text/source/value; concat
selected-row parts get the dangling-table guard on the patch path;
read_semantic_page reports now/count (reverse-map gated so hand-
authored moment formats stay hidden) while concat stays hidden by the
no-raw-bindings default, which the guide now states explicitly.
Council-reviewed (security, architect, QA): unanimous approve-with-
risks; every requested guard, test, and doc landed — including a
table-driven compile+read-back round-trip over all named formats.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
…ored guidance
Three council-reviewed additions closing the remaining capability-audit
and security-follow-up items:
- visibleWhen gains two predicate forms alongside { control, equals }:
{ rowSelected: table } -> {{ T.selectedRowIndex !== -1 }} (show detail
panels/action buttons only with a selection) and { notEmpty: input }
-> {{ !!I.text }}; both guarded against dangling/wrong-type targets,
both read back semantically with isVisible-only scoping (pinned)
- APPSMITH_MCP_STRICT_ELICITATION: in-band approval prompts REQUIRED —
non-elicitation clients refuse with elicitation_required, client_error
never degrades to the relay posture, strict wins over the disable knob
(loud startup warning on the contradictory pair), and refunded client
errors are capped at 3 per session before prompt attempts stop
(security Risk 1 from this review, fixed in-session)
- the bindings guide now warns that read-back hides hand-written
property bindings on ANY human-built page (and that no count reports
them — inspect_page counts trigger bindings only), so agents confirm
before overwriting props on hand-authored apps
Council (security, architect, QA): unanimous approve-with-risks; every
requested fix, test, and doc landed, including the strict happy-path
test and catalog/tool-copy advertising of the new predicates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/29684205123. |
The Build MCP / client-prettier / mcp-build CI jobs (and the on-demand deploy) all fail on 'yarn prettier' over this package because the operator-knob tables added in the elicitation work were hand-aligned. Formatting-only; the husky pre-commit hook lints only ts/js, so the markdown drift never surfaced locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/29686939953. |
|
Deploy-Preview-URL: https://ce-41980.dp.appsmith.com |
The first computation-bearing token in the closed vocabulary: 'show the
temperature in Fahrenheit' is now a structured expression, not a raw-JS
request the server must refuse.
value: { formula: <expr> } where expr is a finite number, a query-field
ref, a selected-row ref, or { op, args } over add/sub/mul/div/round/abs/
min/max. The agent supplies structure; the compiler owns every emitted
character (refs coerced via Number(), compounds parenthesized, the whole
expression finite-guarded so missing/non-numeric data renders blank).
Caps: 25 nodes, depth 6, 8 args, round decimals an integer literal 0-6,
enforced at one point covering both build and patch paths. The patch
path's dangling-table guard now walks concat parts and formula leaves
uniformly (computedValueTableRefs).
Council-reviewed (architect, security, QA — security fuzzed the real
module: extreme literals, token adjacency, cap bombs, 20k-deep parse
DoS; no un-neutralized path). All requested tests landed: every op
branch pinned, boundary acceptance at 8 args/25 nodes/depth 6, isolated
node-cap rejection, a parse-and-evaluate smoke over all 8 ops, and the
semantic fixture built by the real emitter. Emitter round branch gained
a defense-in-depth type assert; deep-payload RangeError mapping tracked
as follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
Reported by Hacktron (medium): the MCP governance audit tools read the mcp_changes collection with no organization predicate. Because isSuperUser is a PER-ORG signal on multi-org (EE) deployments (MANAGE_ORGANIZATION on the caller's own org) and email/actorId is only per-org unique (Migration067), one tenant's admin — or a colliding email — could read another tenant's MCP change history via list_all_changes / get_any_change (and, on collision, list_changes / get_change). Fix (underlying cause, fail-closed at every layer): - McpChangeRecord carries organizationId, stamped on every record at write from the session's tenant. - All four store reads (getChange, listChanges, getAnyChange, listAllChanges) filter by the caller's organization; indexes are org-prefixed. Pre-fix records (no org) match no org-scoped read. - Every audit-read tool refuses with organization_scope_unavailable when the caller's org is unknown, so the empty-org sentinel never runs a read and two tenants can never alias on it — regardless of edition or server version. Governed writes are unaffected. - The caller's tenant is surfaced via a new organizationId on /api/v1/users/me (UserProfileCE_DTO + buildUserProfileDTO), the necessary tenant source for the MCP layer. Regression tests fail on the unpatched code (verified by reverting the filter): real-Mongo cross-tenant isolation (store.integration), always-on tool-layer isolation + fail-closed for admin and actor reads (app.test), a coordinator assertion that org is stamped, and a Java assertion the profile carries organizationId. Council-reviewed (security): APPROVE — reported disclosure fixed and fail-closed at every read path. Ship the server + MCP changes atomically: audit isolation for actor reads is complete only against a server that reports organizationId; otherwise audit reads degrade to unavailable (safe), never a cross-tenant read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/29694771196. |
|
Deploy-Preview-URL: https://ce-41980.dp.appsmith.com |
The token list showed each token by its raw UUID, so users couldn't tell them apart. The create dialog now takes an optional name (shown as the token's title in the list, falling back to the id for older unnamed tokens); a blank name is defaulted server-side to "Token created <date>" so every token stays identifiable. Name is trimmed, stripped of control/ invisible characters, and capped at 50 chars. Backward compatible: existing tokens and no-body create calls keep working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
…atasources Plan to bring the MCP closed vocabulary to ~100% of Appsmith's widgets, table/chart depth, and datasource plugins. Excludes CUSTOM_WIDGET and CUSTOM_FUSION (raw-code, belong to the gated-JS proposal), appsmithAi (per user), and deprecated V1 widgets. Milestoned W1-W7, T1, Ch1, D1-D6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
…widgets (W1) First coverage milestone toward wrapping all safe Appsmith widgets. Five inert display widgets join the closed vocabulary: divider, progress, circularprogress, rate, iconbutton. Schema (bounded value/rate ranges, kebab-case icon charset), templates (matching each widget's getDefaults), catalog + auto-generated guide, and compile+reject tests. Rate declares no client DSL version, so the drift tripwire records it as versionless. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
…tch groups)
Adds seven inert input widgets to the MCP closed vocabulary via the
established 6-step widget-add pattern (schema type + union member +
WidgetSpec shape, template blueprint from each client getDefaults(),
capability catalog entry, DEFAULT_BASE_NAME, builder tests):
- currencyinput (CURRENCY_INPUT_WIDGET) — ISO currency code + decimals
- phoneinput (PHONE_INPUT_WIDGET) — dial-code prefix + auto-format
- numberslider (NUMBER_SLIDER_WIDGET)
- categoryslider (CATEGORY_SLIDER_WIDGET)
- rangeslider (RANGE_SLIDER_WIDGET)
- checkboxgroup (CHECKBOX_GROUP_WIDGET, version 2)
- switchgroup (SWITCH_GROUP_WIDGET)
All free-text fields go through safeText (no {{ }}/${ }/backtick), so
nothing agent-authored can become an evaluated binding. Slider ranges
clamp to a valid track (max>min) defensively; group defaults are
filtered to real option values. Template versions are pinned to each
client widget's getConfig version and guarded by the drift tripwire.
874 mcp tests pass; check-types, eslint, prettier clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
Adds two action widgets to the MCP closed vocabulary: - menubutton (MENU_BUTTON_WIDGET) — a button opening a STATIC dropdown menu built from supplied item labels - buttongroup (BUTTON_GROUP_WIDGET) — a row/column of SIMPLE buttons, each with an optional kebab-case Blueprint icon Item/button labels go through safeText and icons through the kebab-case iconName gate, so nothing agent-authored can become a binding. The id-keyed menuItems / groupButtons maps are generated by the compiler (ids and indexes never come from the agent). Buttons are inert on creation — per-item click actions are wired later via wire_event. 512 builder tests pass; check-types, eslint, prettier clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
…anner) Adds three inert device-capture widgets to the MCP closed vocabulary: - camera (CAMERA_WIDGET) — photo/video capture, optional mirrored preview - audiorecorder (AUDIO_RECORDER_WIDGET) — mic recording (no config) - codescanner (CODE_SCANNER_WIDGET) — QR/barcode scan, ALWAYS_ON or CLICK_TO_SCAN layout All enum fields are constrained literals and the codescanner label goes through safeText. These request device permissions only at view time (viewer consent), so authoring them carries no new surface. Template versions pinned to each widget's getConfig version (drift-guarded). 517 builder tests pass; check-types, eslint, prettier clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
Adds two geographic widgets to the MCP closed vocabulary:
- map (MAP_WIDGET) — an interactive Google map with a settable center,
0-100 zoom, and static pins (lat/long + optional title). Rendering
needs a tenant Google Maps API key in admin settings — org config,
not an authoring concern.
- mapchart (MAP_CHART_WIDGET) — a choropleth over a fixed region set
(WORLD/EUROPE/USA/...) coloured by per-region { id, value } data.
Coordinates are range-checked (lat -90..90, long -180..180); region ids
are charset-gated; titles/labels go through safeText. Colours stay out
of the vocabulary (fixed 3-band ramp). Numeric mapchart values are
coerced to the widget's string form. Versions pinned (drift-guarded).
521 builder tests pass; check-types, eslint, prettier clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
Adds the two hierarchical select widgets to the MCP closed vocabulary:
- singleselecttree (SINGLE_SELECT_TREE_WIDGET) — pick one node
- multiselecttree (MULTI_SELECT_TREE_WIDGET) — pick several nodes
Options are a recursive { label, value, children? } tree (new treeOption
zod schema, exported TreeOption type). Labels/values reuse the safe
select gate at every depth; a supplied default is filtered against the
flattened set of real option values before it is emitted. Versions
pinned to each widget's getConfig version (drift-guarded).
JSON_FORM_WIDGET is deferred: its `schema` is auto-derived from
`sourceData` and embeds internal {{ }} templates, so a correct emit
needs live-DP verification rather than unit tests alone — tracked as a
separate task.
525 builder tests pass; check-types, eslint, prettier clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
…ewer)
Adds four URL-backed media widgets to the MCP closed vocabulary. Unlike
the inert widgets, these carry real surface: an agent-supplied external
URL the viewer's browser loads directly (iframe source, video/audio url,
documentviewer docUrl).
New `safeUrl` gate (schema.ts): the URL must parse as an absolute URL via
WHATWG `new URL()`, its scheme must be http/https (blocks javascript:,
data:, vbscript:, file:, blob:, about:), it must carry no embedded
credentials, and it still runs the RAW_EXPRESSION gate so no {{ }}/
backtick binding can ride in. Raw HTML (iframe srcDoc) is deliberately
NOT exposed — agents never author markup. URLs are emitted as inert
literal props (never registered as dynamic bindings) and are never
fetched server-side, so there is no SSRF surface.
Council-review: senior-architect / security-reviewer / qa-engineer
seated → APPROVE / APPROVE-WITH-RISKS / APPROVE-WITH-RISKS (no BLOCK).
Security judged the gate complete against scheme-normalization tricks.
Applied both non-blocking follow-ups: regression cases pinning the
WHATWG-normalization guarantee (\tjavascript:, JavaScript:, //evil.com,
mailto:, ftp:) and aligned the audio footprint to the client's 28-wide
default. No feature flag — additive and stricter than the native editor.
531 builder tests pass; check-types, eslint, prettier clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
… roadmap Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
Adds an optional `columns: TableColumnSpec[]` to the table widget so an
agent can control column type/label/visibility and add per-row computed
columns, instead of relying on runtime auto-derivation.
Each column is { key, label?, type?, hidden?, computed? }:
- type maps to a safe ColumnTypes subset (text/number/date/image/video/
url/boolean); action/inline-edit column types are deferred.
- computed adds a DERIVED column via a new per-row `cell` leaf in the
shared formula AST: { cell: "price" } -> Number(currentRow["price"]).
The `cell` leaf is guarded — validateFormula(expr, allowCell) rejects it
everywhere except a computed table column, so the already-shipped
text-widget value.formula path is unchanged. columnKey's charset
(alphanumeric/underscore/space) keeps currentRow["..."] unbreakable.
buildTableColumns emits TableWidgetV2 primaryColumns/columnOrder mirroring
the client's getDefaultColumnProperties; it runs from compile.ts once the
table's widgetName is allocated (the computedValue self-references it),
following the list-widget precedent. Each computedValue is registered in
dynamicBindingPathList.
Council-review: security-reviewer / senior-architect / qa-engineer seated
→ APPROVE / APPROVE-WITH-RISKS / APPROVE-WITH-RISKS (no BLOCK). Security
confirmed no charset bypass and an airtight cell-leaf guard. Applied the
follow-ups: a security note + test locking the widgetName-charset
injection invariant, and edge-case tests (sanitize-collision dedup,
omit-columns auto-derive, depth-limit through cell leaves).
PENDING LIVE-DP VERIFICATION (per plan): date/url/media columns may need
type-specific props (inputFormat/displayText) to render; and whether an
explicit column subset suppresses vs auto-appends unlisted data columns.
535 builder tests pass; check-types, eslint, prettier clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N19EppQVZ55hwByVy4HEX8
What & why
Adds an opt-out, same-instance Streamable HTTP MCP server that lets an MCP client (e.g. an AI agent) act as a specific Appsmith user. The client authenticates with a user-scoped bearer token; the Node service forwards that token to the existing
/api/v1endpoints, so Spring Security reconstructs the real user and the existing workspace/app/page ACLs authorize every operation. No privileged or instance-wide credential is used.Resolves #15383.
How it works
Server (CE, EE-overridable)
UserMcpTokendomain + repository + service +McpTokenControllerfor create / list / revoke of user-scoped tokens. Every layer follows the CE-base + thin concrete-subclass split (*CE/*CEImpl) so EE can override.AuthenticationWebFilter(only engages for themcp_prefix) reconstructs the token owner. Invalid / revoked / disabled-user tokens return 401.Migration076creates theuserMcpTokenindexes (the instance runs withauto-index-creation=false, so@Indexedalone is inert).Node service (
app/client/packages/mcp)/healthendpoint, request-body size cap, per-request token revalidation, per-session token binding (constant-time compare), and per-user + global session caps.list_workspaces,list_applications,get_application_context, andimport_application_artifact/import_partial_application_artifact— writes go through the existing validated import / partial-import APIs (no raw DSL/Mongo writes).Client
Rollout / deploy
APPSMITH_MCP_ENABLED=1gates both the supervisord autostart and the Caddy/mcproute; a disabled instance never starts the Node process and returns 404 for/mcp. Existing instances are unaffected until an admin opts in and a user deliberately issues + uses a token.mcp-buildCI workflow, and a/mcp/healthroute test.Testing
mcp_/ anonymous → 401), session binding & revalidation, TTL/expiry, per-user (429) and global (503) caps, artifact validation, malformed/oversized body handling. All pass.check-types, ESLint (0 errors), and the MCP package typecheck locally.Security review
Reviewed by a multi-agent council (architecture, security, QA, data-migration, DX, UX, product). Key hardening landed from that review: 401 (not 500) on bad tokens, the CE/EE split, the index migration, the
mcp_-prefix + non-anonymous/megate (closes an unauthenticated-session DoS), per-user session cap, and the opt-in deploy gate.Follow-ups (tracked, not blocking an opt-in ship)
🤖 Generated with Claude Code
Automation
/ok-to-test tags="@tag.All"
Summary by CodeRabbit
Summary
/mcpand/mcp/healthrouting, included in Docker images/packaging workflows.Tip
🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
Workflow run: https://github.com/appsmithorg/appsmith/actions/runs/29714179385
Commit: a062fd5
Cypress dashboard.
Tags:
@tag.AllSpec:
Mon, 20 Jul 2026 04:27:46 UTC