-
Notifications
You must be signed in to change notification settings - Fork 4.8k
feat: Mcp server client changes #42187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b80887e
d5dbc1b
d2f1190
a7e9968
645813f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import Api from "api/Api"; | ||
| import type { ApiResponse } from "api/ApiResponses"; | ||
|
|
||
| export type McpKeyStatus = "ACTIVE" | "REVOKED" | "EXPIRED"; | ||
|
|
||
| export interface McpTokenMetadata { | ||
| // The server serializes Instant fields as epoch seconds (a number); older/other paths may send an ISO string. | ||
| createdAt: string | number; | ||
| expiresAt: string | number; | ||
| id: string; | ||
| // User-facing label. Absent on tokens created before naming existed; the UI falls back to the id then. | ||
| name?: string; | ||
| // Derived on the server. List currently omits revoked keys, so REVOKED will not appear until that query changes. | ||
| status?: McpKeyStatus; | ||
| } | ||
|
|
||
| export interface CreatedMcpToken extends McpTokenMetadata { | ||
| token: string; | ||
| } | ||
|
|
||
| class McpTokenApi extends Api { | ||
| static url = "v1/users/mcp-tokens"; | ||
|
|
||
| // name is optional; a blank/absent name is defaulted server-side to "Token created <date>". | ||
| // keySpanDays must be one of 30, 60, 90, 180, 365; the server defaults to 30 if omitted. | ||
| static async create( | ||
| name?: string, | ||
| keySpanDays?: number, | ||
| ): Promise<ApiResponse<CreatedMcpToken>> { | ||
| const trimmed = name?.trim(); | ||
| const response = await Api.post(McpTokenApi.url, { | ||
| ...(trimmed ? { name: trimmed } : {}), | ||
| ...(keySpanDays != null ? { keySpanDays } : {}), | ||
| }); | ||
|
|
||
| return response as unknown as ApiResponse<CreatedMcpToken>; | ||
| } | ||
|
|
||
| // One envelope whose `data` holds the whole list, matching every other list endpoint. (The server used to return | ||
| // Flux<ResponseDTO<T>> — a bare array of N envelopes — which had no top-level responseMeta for the shared | ||
| // response interceptor to validate.) | ||
| static async list(): Promise<ApiResponse<McpTokenMetadata[]>> { | ||
| const response = await Api.get(McpTokenApi.url); | ||
|
|
||
| return response as unknown as ApiResponse<McpTokenMetadata[]>; | ||
| } | ||
|
|
||
| static async rotate(tokenId: string): Promise<ApiResponse<CreatedMcpToken>> { | ||
| const response = await Api.post(`${McpTokenApi.url}/${tokenId}/rotate`); | ||
|
|
||
| return response as unknown as ApiResponse<CreatedMcpToken>; | ||
| } | ||
|
|
||
| static async revoke(tokenId: string): Promise<ApiResponse<boolean>> { | ||
| const response = await Api.delete(`${McpTokenApi.url}/${tokenId}`); | ||
|
|
||
| return response as unknown as ApiResponse<boolean>; | ||
| } | ||
| } | ||
|
|
||
| export default McpTokenApi; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| /** | ||
| * Default MCP endpoint advertised to clients. The /mcp route is served from the app origin (via Caddy). | ||
| */ | ||
| export const getDefaultMcpServerUrl = (): string => | ||
| `${window.location.origin}/mcp`; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,16 +20,24 @@ import store from "store"; | |
| import { isMultiOrgFFEnabled } from "ee/utils/planHelpers"; | ||
| import { getCurrentUser } from "selectors/usersSelectors"; | ||
| import { getShowAdminSettings } from "ee/utils/BusinessFeatures/adminSettingsHelpers"; | ||
| import { | ||
| getMcpServerConfig, | ||
| mcpKeys, | ||
| } from "ee/pages/AdminSettings/config/mcpServer"; | ||
| import { getIsMcpEnabled } from "ee/selectors/organizationSelectors"; | ||
|
|
||
| const featureFlags = selectFeatureFlags(store.getState()); | ||
| const isMultiOrgEnabled = isMultiOrgFFEnabled(featureFlags); | ||
| const isMCPEnabled = getIsMcpEnabled(store.getState()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# Description: Determine whether organizationConfiguration is in the store before ce/pages/AdminSettings/config/index.ts is imported.
set -uo pipefail
echo "=== Importers of the admin settings config module ==="
rg -n --type=ts --type=tsx -C4 'AdminSettings/config(/index)?["'\'']' app/client/src || true
echo
echo "=== Where FETCH_CURRENT_ORGANIZATION_CONFIG is dispatched ==="
rg -n --type=ts --type=tsx -C6 'FETCH_CURRENT_ORGANIZATION_CONFIG|getCurrentOrganization\s*\(' app/client/src || true
echo
echo "=== Reducer that populates organizationConfiguration ==="
fd -e ts -e tsx . app/client/src --exec rg -ln 'organizationConfiguration' {} \; | while IFS= read -r f; do
echo "--- $f ---"
rg -n -C4 'organizationConfiguration' "$f"
done
echo
echo "=== ConfigFactory.register / getCategory surface ==="
fd -i 'ConfigFactory' app/client/src --exec ast-grep outline {} --items all \;Repository: appsmithorg/appsmith Length of output: 13088 🏁 Script executed: #!/bin/bash
set -uo pipefail
echo "=== Target config module and direct importers ==="
cat -n app/client/src/ce/pages/AdminSettings/config/index.ts
rg -n -C5 --glob '*.ts' --glob '*.tsx' 'pages/AdminSettings/config|AdminSettings/config' app/client/src/ce app/client/src/ee app/client/src/pages
echo
echo "=== Admin settings route and bootstrap/import order ==="
rg -n -C8 --glob '*.ts' --glob '*.tsx' 'AdminSettings|FETCH_CURRENT_ORGANIZATION_CONFIG|getCurrentOrganization' app/client/src/routes app/client/src/ce app/client/src/ee app/client/src/pages 2>/dev/null || true
echo
echo "=== Organization reducer action bindings ==="
cat -n app/client/src/ce/reducers/organizationReducer.ts
rg -n -C6 --glob '*.ts' --glob '*.tsx' 'FETCH_CURRENT_ORGANIZATION_CONFIG|FETCH_CURRENT_ORGANIZATION_CONFIG_SUCCESS|FETCH_CURRENT_ORGANIZATION_CONFIG_ERROR' app/client/src/ce app/client/src/ee app/client/src/store app/client/src 2>/dev/null | head -240
echo
echo "=== ConfigFactory registration and category consumers ==="
cat -n app/client/src/pages/AdminSettings/config/ConfigFactory.ts
rg -n -C5 --glob '*.ts' --glob '*.tsx' 'getCategory\(|categories|registerCategory|registerSettings' app/client/src/pages/AdminSettings app/client/src/ce/pages/AdminSettings app/client/src/ee/pages/AdminSettings 2>/dev/null | head -260
echo
echo "=== Applicable repository conventions and learnings ==="
find /tmp/coderabbit-repo-knowledge/appsmithorg-appsmith-9bcd6238 -maxdepth 2 -type f -name '*.md' -printRepository: appsmithorg/appsmith Length of output: 50377 🏁 Script executed: #!/bin/bash
set -uo pipefail
log=/tmp/coderabbit-shell-logs/shell-output-Weo8ia
echo "=== Target config module ==="
sed -n '/^=== Target config module and direct importers ===/,/^=== Admin settings route and bootstrap\/import order ===/p' "$log" | head -220
echo
echo "=== Admin settings page and loader ==="
cat -n app/client/src/pages/AdminSettings/index.tsx
cat -n app/client/src/pages/AdminSettings/loader.tsx
echo
echo "=== Organization fetch saga and its callers ==="
cat -n app/client/src/ce/sagas/organizationSagas.tsx
rg -n -C8 --glob '*.ts' --glob '*.tsx' 'FETCH_CURRENT_ORGANIZATION_CONFIG' app/client/src/ce app/client/src/ee app/client/src/pages app/client/src/actions app/client/src/store 2>/dev/null | head -220
echo
echo "=== ConfigFactory category retrieval ==="
sed -n '1,180p' app/client/src/pages/AdminSettings/config/ConfigFactory.ts
rg -n -C8 --glob '*.ts' --glob '*.tsx' 'getCategoryDetails|getCategory\(' app/client/src/pages/AdminSettingsRepository: appsmithorg/appsmith Length of output: 50376 🏁 Script executed: #!/bin/bash
set -uo pipefail
echo "=== Organization configuration defaults ==="
sed -n '1,125p' app/client/src/ce/reducers/organizationReducer.ts
rg -n -C10 --glob '*.ts' --glob '*.tsx' 'defaultBrandingConfig|mcpConfig' app/client/src/ce app/client/src/ee | head -220
echo
echo "=== Every production dispatch/call of getCurrentOrganization ==="
rg -n -C8 --glob '*.ts' --glob '*.tsx' --glob '!*.test.*' 'getCurrentOrganization\s*\(' app/client/src/ce app/client/src/ee app/client/src/pages app/client/src/sagas app/client/src/actions | head -260
echo
echo "=== Direct organization action dispatches ==="
rg -n -C8 --glob '*.ts' --glob '*.tsx' --glob '!*.test.*' 'FETCH_CURRENT_ORGANIZATION_CONFIG' app/client/src/ce app/client/src/ee app/client/src/pages app/client/src/sagas app/client/src/actions | head -300
echo
echo "=== MCP page behavior ==="
cat -n app/client/src/pages/AdminSettings/Profile/McpKeysPage.tsxRepository: appsmithorg/appsmith Length of output: 50376 🏁 Script executed: #!/bin/bash
set -uo pipefail
echo "=== Initialization flow around organizationConfig ==="
sed -n '250,380p' app/client/src/sagas/InitSagas.ts
rg -n -C12 --glob '*.ts' --glob '*.tsx' 'organizationConfig\s*[:=]|consolidated-api|startAppEngine|INIT_APP' app/client/src/sagas app/client/src/api app/client/src/actions | head -320
echo
echo "=== MCP configuration and category shape ==="
cat -n app/client/src/ce/pages/AdminSettings/config/mcpServer.ts
rg -n -C12 --glob '*.ts' --glob '*.tsx' 'mcpKeys|MCP_KEYS|SettingCategories.PROFILE|categoryType' app/client/src/ce/pages/AdminSettings/config app/client/src/pages/AdminSettings/LeftPane.tsx app/client/src/pages/AdminSettings/Main.tsx
echo
echo "=== MCP page selector and mount effects ==="
sed -n '650,755p' app/client/src/pages/AdminSettings/Profile/McpKeysPage.tsxRepository: appsmithorg/appsmith Length of output: 50376 🏁 Script executed: #!/bin/bash
set -uo pipefail
echo "=== Admin settings save path ==="
sed -n '74,155p' app/client/src/pages/AdminSettings/SettingsForm.tsx
rg -n -C10 --glob '*.ts' --glob '*.tsx' 'needsRefresh|mcpConfig.enabled|MCP_ENABLED_SETTING' app/client/src/pages/AdminSettings app/client/src/ce/pages/AdminSettings app/client/src/ee/pages/AdminSettings | head -240Repository: appsmithorg/appsmith Length of output: 21726 🏁 Script executed: #!/bin/bash
set -uo pipefail
echo "=== All MCP enablement uses in McpKeysPage ==="
rg -n -C6 'isMcpEnabled|getIsMcpEnabled|return null' app/client/src/pages/AdminSettings/Profile/McpKeysPage.tsxRepository: appsmithorg/appsmith Length of output: 2333 Register
🤖 Prompt for AI Agents |
||
| const user = getCurrentUser(store.getState()); | ||
| const isFeatureEnabled = featureFlags.license_gac_enabled; | ||
| const isSuperUser = getShowAdminSettings(isFeatureEnabled, user); | ||
|
|
||
| // Profile categories | ||
| ConfigFactory.register(ProfileConfig); | ||
|
|
||
| if (isMCPEnabled) ConfigFactory.register(mcpKeys); | ||
|
|
||
| // Organisation categories | ||
| if (isSuperUser) ConfigFactory.register(GeneralConfig); | ||
|
|
||
|
|
@@ -41,6 +49,9 @@ if (isSuperUser) ConfigFactory.register(AuditLogsConfig); | |
|
|
||
| if (isSuperUser) ConfigFactory.register(AIConfig); | ||
|
|
||
| if (isSuperUser && isMultiOrgEnabled) | ||
| ConfigFactory.register(getMcpServerConfig(isMultiOrgEnabled)); | ||
|
|
||
| // User management categories | ||
| if (isSuperUser) ConfigFactory.register(UserSettings); | ||
|
|
||
|
|
@@ -55,6 +66,9 @@ if (isSuperUser && !isMultiOrgEnabled) ConfigFactory.register(InstanceSettings); | |
|
|
||
| if (isSuperUser && !isMultiOrgEnabled) ConfigFactory.register(Configuration); | ||
|
|
||
| if (isSuperUser && !isMultiOrgEnabled) | ||
| ConfigFactory.register(getMcpServerConfig(isMultiOrgEnabled)); | ||
|
|
||
| if (isSuperUser && !isMultiOrgEnabled) ConfigFactory.register(VersionConfig); | ||
|
|
||
| export default ConfigFactory; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import type { | ||
| AdminConfigType, | ||
| Setting, | ||
| } from "ee/pages/AdminSettings/config/types"; | ||
| import { | ||
| CategoryType, | ||
| SettingCategories, | ||
| SettingTypes, | ||
| SettingSubtype, | ||
| } from "ee/pages/AdminSettings/config/types"; | ||
| import { getDefaultMcpServerUrl } from "ee/constants/mcp"; | ||
| import McpKeysPage from "pages/AdminSettings/Profile/McpKeysPage"; | ||
|
|
||
| const isMcpServerOff = ( | ||
| // TODO: Fix this the next time the file is edited | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| values?: Record<string, any>, | ||
| ) => | ||
| values?.mcpConfig?.enabled !== true && values?.["mcpConfig.enabled"] !== true; | ||
|
|
||
| export const MCP_ENABLED_SETTING: Setting = { | ||
| id: "mcpConfig.enabled", | ||
| name: "mcpConfig.enabled", | ||
| category: SettingCategories.MCP_SERVER, | ||
| controlType: SettingTypes.TOGGLE, | ||
| label: "Enable MCP server", | ||
| text: "Allow AI agents to connect to this organization over MCP (Model Context Protocol)", | ||
| helpText: | ||
| "* Agents authenticate with per-user MCP keys (Settings → MCP Keys) and act with that user's permissions. Disabled by default — turning this on exposes the /mcp endpoint and lets users create MCP keys. Turning it off removes the endpoint, blocks new keys, and rejects existing ones.", | ||
| defaultValue: false, | ||
| }; | ||
|
|
||
| export const MCP_DATA_ENABLED_SETTING: Setting = { | ||
| id: "mcpConfig.dataEnabled", | ||
| name: "mcpConfig.dataEnabled", | ||
| category: SettingCategories.MCP_SERVER, | ||
| controlType: SettingTypes.TOGGLE, | ||
| label: "MCP data tools", | ||
| text: "Let agents work with datasources and queries (create datasources/queries, run read-only actions)", | ||
| helpText: | ||
| "* Disabled by default. Requires the MCP server above. All operations run under the connecting user's existing permissions; credentials are never exposed to agents.", | ||
| defaultValue: false, | ||
| isDisabled: isMcpServerOff, | ||
| }; | ||
|
|
||
| export const MCP_SERVER_URL_SETTING: Setting = { | ||
| id: "mcpConfig.serverUrl", | ||
| name: "mcpConfig.serverUrl", | ||
| category: SettingCategories.MCP_SERVER, | ||
| controlType: SettingTypes.TEXTINPUT, | ||
| controlSubType: SettingSubtype.TEXT, | ||
| label: "MCP server URL", | ||
| subText: "MCP server URL which MCP clients should use to reach this instance", | ||
| placeholder: getDefaultMcpServerUrl(), | ||
| helpText: | ||
| "* URL MCP clients should use to reach this instance. Leave blank to use the default (this origin + /mcp). Set a custom value if Appsmith is behind a reverse proxy or a different public hostname.", | ||
| isDisabled: isMcpServerOff, | ||
|
|
||
| validate: (value: string) => { | ||
| if (value === undefined || value === "") { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const url = new URL(value); | ||
|
|
||
| if (url.protocol !== "http:" && url.protocol !== "https:") { | ||
| return "Enter an http(s) URL."; | ||
| } | ||
| } catch { | ||
| return "Enter a valid URL."; | ||
| } | ||
| }, | ||
| }; | ||
|
|
||
| const instanceSettings = [ | ||
| MCP_ENABLED_SETTING, | ||
| MCP_DATA_ENABLED_SETTING, | ||
| MCP_SERVER_URL_SETTING, | ||
| ]; | ||
|
|
||
| export const mcpKeys: AdminConfigType = { | ||
| icon: "robot-2", | ||
| type: SettingCategories.MCP_KEYS, | ||
| categoryType: CategoryType.PROFILE, | ||
| controlType: SettingTypes.PAGE, | ||
| component: McpKeysPage, | ||
| title: "MCP keys", | ||
| canSave: false, | ||
| } as AdminConfigType; | ||
|
|
||
| export const config: AdminConfigType = { | ||
| icon: "robot-2", | ||
| type: SettingCategories.MCP_SERVER, | ||
| categoryType: CategoryType.ORGANIZATION, | ||
| controlType: SettingTypes.GROUP, | ||
| title: "MCP Server (BETA)", | ||
| canSave: true, | ||
| settings: [MCP_ENABLED_SETTING, MCP_DATA_ENABLED_SETTING], | ||
| }; | ||
|
|
||
| export const getMcpServerConfig = ( | ||
| isMultiOrgEnabled: boolean, | ||
| ): AdminConfigType => { | ||
| return isMultiOrgEnabled | ||
| ? { ...config, settings: [MCP_ENABLED_SETTING] } | ||
| : { | ||
| ...config, | ||
| categoryType: CategoryType.INSTANCE, | ||
| settings: instanceSettings, | ||
| }; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Align the user-facing term for one concept.
The new strings use two words for the same object. The page title is "MCP keys" (line 241) and the create action is "Create Key" (lines 257 and 259), but the reveal field is "MCP token" (line 269), the copy toast is "MCP token copied" (line 271), and the empty state is "No MCP tokens have been created." (line 281). A user sees both "key" and "token" on the same screen.
Pick one term for the UI copy. Keep the internal identifiers as they are if renaming them is out of scope for this PR.
📝 Example alignment on "key"
Note:
McpKeysPage.test.tsxasserts several of these strings. Update the assertions at lines 187, 222, 229, 240, 349, 371, 381, 387, and 402 together with the copy.Also applies to: 257-259, 269-272, 281-281
🤖 Prompt for AI Agents