feat(bitrix24): replace two MCP text inputs with a filtered dropdown (#1392)

* feat(bitrix24): replace two MCP text inputs with a filtered dropdown [B24:2794]

Bitrix24 channel creation used to demand two hand-typed strings —
mcp_server_name and mcp_base_url — plus zero indication of which MCP
servers can actually auto-onboard. Typos silently disabled provisioning
and admin had to know which servers implement /api/auto-onboard.

Ship a single dropdown backed by mcp_servers.require_user_credentials,
plus the machinery to make it work end-to-end.

Phase 1 — DB & store
* Promote require_user_credentials from settings JSONB to a top-level
  column on mcp_servers (PG migration 000089, SQLite migration v54).
* Backfill from existing settings blobs so no admin needs to re-tick.
* Add MCPServerData.RequireUserCredentials to the Go store layer, plumb
  through Create / Get / GetByName / List / Update on both stores,
  extend the export DTO, and add require_user_credentials to the HTTP
  allowlist.
* Bump RequiredSchemaVersion 87 -> 89 (jumping 88, which was on disk
  but not wired) and SchemaVersion 53 -> 54 with an
  idempotentColumnMigration guard.

Phase 2 — Bitrix24 channel factory
* Add MCPServerID (UUID string) to bitrix24 InstanceConfig, keep
  MCPServerName + MCPBaseURL as legacy fallback with a "deprecated"
  doc comment.
* Factory validation accepts either mcp_server_id alone or the legacy
  pair; half-config still fails fast.
* initMCPProvisioner prefers GetServer(id) and sources the base URL
  from mcp_servers.url when the id path is used. Legacy name path
  unchanged so pre-migration configs keep working.
* Log line now carries mcp_server_id + require_user_credentials so
  operators can eyeball the wiring.

Phase 3 — Frontend types & MCP form
* Add optional top-level require_user_credentials to MCPServerData /
  MCPServerInput in both ui/web and ui/desktop/frontend types.
* mcp-form-dialog reads the top-level flag first and falls back to
  settings.require_user_credentials so cached responses from
  pre-upgrade backends still render correctly.
* On submit send both the top-level flag AND the legacy settings
  entry so mid-rollout backends stay consistent.

Phase 4 — Bitrix24 channel form dropdown
* New mcp-select field type + MCPServerSelect component. Uses the
  shared useMCP() react-query cache and filters client-side to
  servers whose require_user_credentials is true (OR settings
  JSONB during the migration window).
* Explicit "None (disable MCP provisioning)" option so admins can
  clear the binding without editing config JSON.
* Legacy mcp_server_name / mcp_base_url text inputs kept in the
  Advanced panel, relabelled "(legacy)" with pointer help text.

Phase 5 — channel_instances.config backfill
* PG migration 000090 and SQLite migration v55 rewrite existing
  bitrix24 channel_instances.config to add mcp_server_id by
  resolving mcp_server_name against mcp_servers, tenant-scoped
  via agents.tenant_id (channel_instances doesn't carry tenant_id
  directly).
* Idempotent — only touches rows already carrying
  mcp_server_name that lack mcp_server_id. Legacy keys are left
  in place so provisioner.go can still fall back for unmigrated
  or future-created legacy configs.
* down.sql drops the mcp_server_id key. Provisioner immediately
  reverts to the legacy fallback path.

Tests
* provisioner_test.go: three new cases exercise the mcp_server_id
  path (invalid UUID string, valid UUID with missing row, valid
  UUID with a per-user row). fakeMCPStore gains a serversByID
  map and a real GetServer implementation.
* Existing legacy-config tests unchanged and still green.

Verification
* go build ./... && go build -tags sqliteonly ./...
* go vet ./internal/mcp/... ./internal/channels/bitrix24/...
    ./internal/store/... ./internal/http/...
* go test ./internal/mcp/... ./internal/channels/bitrix24/...  -> ok
* Live-tested against a local docker image on the goclaw-deploy
  postgres. Migrations 89 + 90 applied cleanly. Three existing
  bitrix24 channels (bitrix-sales / nguyen-dao-openline / tieu-vi)
  had their configs backfilled with the b24-syn-mcp UUID and the
  provisioner boots with require_user_credentials=true. UI dropdown
  correctly shows only b24-syn-mcp (the only server with the flag
  ticked) alongside a "None" clearer option.

Surface parity
* Gateway server: store + factory + provisioner + HTTP allowlist.
* API contract: adds require_user_credentials + mcp_server_id
  as optional fields on existing routes. No new endpoints.
* Web UI: MCP form + Bitrix24 channel form + shared types.
* CLI/runtime package: N/A because no CLI subcommand reads the
  mcp_server_id field.

* fix(bitrix24): derive auto-onboard base URL from mcp_servers.url origin [B24:2794]

The Phase 2 refactor swapped provisioner base-URL sourcing from the
legacy per-channel MCPBaseURL config field (which historically stored the
MCP server's ORIGIN, e.g. https://mcp.example.com) to mcp_servers.url,
which stores the JSON-RPC ENDPOINT the agent loop dials (e.g.
https://mcp.example.com/mcp). The two are semantically different but
share a single column.

mcp_client.newMCPClient then appends "/api/auto-onboard" to whatever
baseURL it receives, so the id-path started POSTing to
".../mcp/api/auto-onboard" — 404 for every per-user credential mint and
refresh. Existing users kept working only until their cached access
tokens expired.

Fix: derive the origin (scheme://host[:port]) from server.URL before
handing it to the auto-onboard client. The legacy path is untouched
because MCPBaseURL from channel config is already the origin.

  https://b24-mcp-dev.synity.so/mcp   ->  https://b24-mcp-dev.synity.so
  https://mcp.example.com/mcp/        ->  https://mcp.example.com
  https://mcp.example.com             ->  https://mcp.example.com

Table-driven test covers six shapes plus four error cases (empty,
whitespace-only, no scheme, no host). Updated the existing
TestInitMCPProvisioner_MCPServerID fixture to seed a URL with the /mcp
subpath so it regression-guards the same code path.

Verified live: user 614 sent a message that triggered the expired-cred
refresh branch; goclaw logged "self-refreshed user credentials
created=false" and the agent immediately reported
"mcp.user_tools_loaded user=614 tools=2". Before this fix the same event
logged 'auto-onboard failed: mcp auto-onboard: 404 Not Found'.

Surface parity:
  - Gateway server: provisioner + one new helper (deriveAutoOnboardBaseURL).
  - API contract: N/A because the wire shape hasn't changed.
  - Web UI: N/A because the UI still writes mcp_server_id verbatim.
  - CLI/runtime: N/A because no CLI reads the derived base URL.

---------

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
This commit is contained in:
SYNITYandDangTinh311 authored and GitHub committed 2026-07-08 19:23:21 +07:00
1 parent 7263f771c8
commit 3b8bc1bc4d
24 files changed
+561 -85

No files matched your search

+4
View File
@@ -12,6 +12,8 @@ export interface MCPServerData {
timeout_sec: number
settings?: { require_user_credentials?: boolean }
enabled: boolean
/** Top-level twin of `settings.require_user_credentials` (Phase 89 rollout). */
require_user_credentials?: boolean
created_by: string
agent_count?: number
created_at: string
@@ -30,6 +32,8 @@ export interface MCPServerInput {
tool_prefix?: string
timeout_sec?: number
enabled?: boolean
/** Top-level flag written since Phase 89; backends still accept the legacy JSONB entry. */
require_user_credentials?: boolean
}
export interface MCPAgentGrant {
@@ -0,0 +1,70 @@
import { useMemo } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useMCP } from "@/pages/mcp/hooks/use-mcp";
import type { MCPServerData } from "@/types/mcp";
/**
* Dropdown picker over MCP servers whose `require_user_credentials` flag is
* true — the population channels can meaningfully auto-onboard against.
*
* The list comes from the same `useMCP()` query the MCP admin page uses so
* cache is shared and the filter is a cheap client-side pass (~10 servers
* per portal on realistic setups). Legacy `mcp_server_name` fallback is
* handled at the Bitrix24 factory layer; this component only writes UUIDs.
*/
interface MCPServerSelectProps {
value: string;
onChange: (value: string | undefined) => void;
placeholder?: string;
disabled?: boolean;
}
const CLEAR_VALUE = "__none__";
export function MCPServerSelect({ value, onChange, placeholder, disabled }: MCPServerSelectProps) {
const { servers, loading } = useMCP();
const perUserServers = useMemo<MCPServerData[]>(
() =>
(servers ?? []).filter(
(s) => s.require_user_credentials || s.settings?.require_user_credentials,
),
[servers],
);
const selectValue = value && value !== "" ? value : CLEAR_VALUE;
return (
<div className="grid gap-1.5">
<Select
value={selectValue}
onValueChange={(next) => onChange(next === CLEAR_VALUE ? undefined : next)}
disabled={disabled || loading}
>
<SelectTrigger>
<SelectValue placeholder={placeholder ?? (loading ? "Loading MCP servers…" : "Select an MCP server (optional)")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={CLEAR_VALUE}>— None (disable MCP provisioning) —</SelectItem>
{perUserServers.length === 0 && !loading && (
<SelectItem value="__empty__" disabled>
No per-user MCP servers registered
</SelectItem>
)}
{perUserServers.map((srv) => (
<SelectItem key={srv.id} value={srv.id}>
{srv.display_name?.trim() || srv.name}
{srv.url ? <span className="ml-2 text-xs text-muted-foreground">({srv.url})</span> : null}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}
@@ -17,6 +17,7 @@ import { SkillNameSelect } from "@/components/shared/skill-name-select";
import { ProviderModelSelect } from "@/components/shared/provider-model-select";
import { InfoLabel } from "@/components/shared/info-label";
import { BitrixPortalSelect } from "./bitrix24/bitrix-portal-select";
import { MCPServerSelect } from "./bitrix24/mcp-server-select";
import { deliveryModelKey, isDeliveryModelKey, isDeliveryProviderKey } from "./channel-delivery-provider-fields";
import type { FieldDef } from "./channel-schemas";
@@ -424,6 +425,19 @@ function FieldRenderer({
</div>
);
case "mcp-select":
return (
<div className="grid gap-1.5">
<FieldLabel htmlFor={id} text={label} tip={tooltipHelp} />
<MCPServerSelect
value={(value as string) ?? ""}
onChange={onChange}
placeholder={field.placeholder}
/>
{inlineHelp && <p className="text-xs text-muted-foreground">{inlineHelp}</p>}
</div>
);
case "tags":
return (
<div className="grid gap-1.5">
+9 -3
View File
@@ -5,7 +5,7 @@ import { reasoningDeliveryOptions } from "./reasoning-delivery-config";
export interface FieldDef {
key: string;
label: string;
type: "text" | "password" | "number" | "boolean" | "select" | "multi-select" | "tags" | "tristate" | "textarea" | "tool-select" | "skill-select";
type: "text" | "password" | "number" | "boolean" | "select" | "multi-select" | "tags" | "tristate" | "textarea" | "tool-select" | "skill-select" | "mcp-select";
placeholder?: string;
required?: boolean;
defaultValue?: string | number | boolean | string[];
@@ -298,8 +298,14 @@ export const configSchema: Record<string, FieldDef[]> = {
{ key: "allow_from", label: "Allowed Users (DM)", type: "tags", help: "Bitrix24 user IDs allowed to DM the bot. Empty = no allowlist filter." },
{ key: "group_allow_from", label: "Allowed Users (Group)", type: "tags", help: "Separate allowlist for group senders." },
...chatBehaviorOverrideFields,
{ key: "mcp_server_name", label: "MCP Server Name", type: "text", advanced: true, placeholder: "bitrix24-prod", help: "Optional — name from mcp_servers table. Must be set together with MCP Base URL to enable per-user MCP credential auto-onboard. Leave both empty to disable." },
{ key: "mcp_base_url", label: "MCP Base URL", type: "text", advanced: true, placeholder: "https://mcp.example.com", help: "Optional — HTTPS root of the partner MCP server. Channel POSTs {mcp_base_url}/api/auto-onboard to mint per-user credentials on first-sight. The MCP server authenticates each call via the caller's Bitrix access_token, so no admin secret is required." },
// Preferred single-select input backed by mcp_servers.require_user_credentials.
// Base URL is resolved from the selected row at Start() — no separate field needed.
{ key: "mcp_server_id", label: "MCP Server", type: "mcp-select", help: "Optional — pick a per-user MCP server (only servers with \"Require user credentials\" ticked appear here). Channel POSTs {server.url}/api/auto-onboard on first-sight to mint the caller's per-user credential. Leave empty to disable MCP provisioning for this channel." },
// Legacy string fields — kept for backward-compat with configs written before
// the mcp_server_id rollout. Phase 5 migrates every existing channel to
// mcp_server_id and drops these entries.
{ key: "mcp_server_name", label: "MCP Server Name (legacy)", type: "text", advanced: true, placeholder: "bitrix24-prod", help: "Deprecated — use \"MCP Server\" dropdown instead. Kept for backward-compat with pre-Phase-89 configs; will be removed once every channel is migrated." },
{ key: "mcp_base_url", label: "MCP Base URL (legacy)", type: "text", advanced: true, placeholder: "https://mcp.example.com", help: "Deprecated — see the \"MCP Server\" dropdown. Legacy base URL used only when the new dropdown is empty and the legacy name is set." },
],
};
+8 -1
View File
@@ -100,7 +100,10 @@ export function MCPFormDialog({ open, onOpenChange, server, onSubmit, onTest, on
toolPrefix: server?.tool_prefix ?? "",
timeout: server?.timeout_sec ?? 60,
enabled: server?.enabled ?? true,
requireUserCreds: server?.settings?.require_user_credentials ?? false,
// Prefer the top-level column (Phase 89 backfilled from settings JSONB);
// fall back to the legacy settings entry so cached responses from
// pre-upgrade backends still render the checkbox with the right value.
requireUserCreds: server?.require_user_credentials ?? server?.settings?.require_user_credentials ?? false,
toolHintsGlobal: server?.settings?.tool_hints?.global ?? "",
toolHintsTools: server?.settings?.tool_hints?.tools ?? {},
oauthEnabled: oauth?.auth_type === "oauth",
@@ -190,6 +193,10 @@ export function MCPFormDialog({ open, onOpenChange, server, onSubmit, onTest, on
timeout_sec: data.timeout,
settings,
enabled: data.enabled,
// Send the promoted top-level flag alongside the legacy settings
// twin so both backends (pre- and post-Phase 89) stay in sync during
// the migration window. Phase 5 drops the settings entry.
require_user_credentials: data.requireUserCreds,
};
};
+14
View File
@@ -41,6 +41,14 @@ export interface MCPServerData {
timeout_sec: number;
settings?: MCPServerSettings;
enabled: boolean;
/**
* True when the server mints credentials per-user at message time
* (Bitrix24 auto-onboard etc.). Promoted from `settings.require_user_credentials`
* to a top-level column; the settings entry stays for one release cycle
* so legacy readers keep working. Prefer this field over the settings
* entry when both are present.
*/
require_user_credentials?: boolean;
created_by: string;
agent_count?: number;
created_at: string;
@@ -60,6 +68,12 @@ export interface MCPServerInput {
timeout_sec?: number;
settings?: MCPServerSettings;
enabled?: boolean;
/**
* Top-level twin of `settings.require_user_credentials`. Send both while
* the server backend is still on the migration window (readers may hit
* either field); after Phase 5 lands the settings entry disappears.
*/
require_user_credentials?: boolean;
}
export interface MCPToolInfo {