mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-18 12:14:24 +00:00
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:
1 parent
7263f771c8
commit
3b8bc1bc4d
24 files changed
+561
-85
No files matched your search
@@ -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">
|
||||
|
||||
@@ -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." },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user