mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add model_intent option to MCP server configs (#23717)
Add a per-MCP-server `model_intent` toggle that wraps tool schemas with
a
`model_intent` field, requiring the LLM to provide a human-readable
description of each tool call's purpose. The intent string is shown as a
status label in the UI instead of opaque tool names, and is
transparently
stripped before the call reaches the remote MCP server.
Built-in tools have rich specialized renderers (terminal blocks, file
diffs,
etc.) and don't need this. MCP tools hit `GenericToolRenderer` which
only
shows raw tool names and JSON — that's where model_intent adds value.
The model learns what to provide via the JSON Schema `description` on
the
`model_intent` property itself — no system prompt changes needed.
<details>
<summary>Implementation details</summary>
### Architecture
Inspired by the `withModelIntent()` pattern from `coder/blink`, adapted
for
Go + React. The wrapping is entirely in the `mcpclient` layer — tool
implementations never see `model_intent`.
**Schema wrapping** (`mcpToolWrapper.Info()`): When enabled, wraps the
original tool parameters under a `properties` key and adds a
`model_intent`
string field with a rich description that teaches the model inline.
**Input unwrapping** (`mcpToolWrapper.Run()`): Strips `model_intent` and
unwraps `properties` before forwarding to the remote MCP server. Handles
three input shapes models may produce:
1. `{ model_intent, properties: {...} }` — correct format
2. `{ model_intent, key: val, ... }` — flat, no wrapper
3. Malformed — falls through gracefully
**Frontend extraction**: `streamState.ts` extracts `model_intent` from
incrementally parsed streaming JSON. `messageParsing.ts` extracts it
from
persisted tool call args.
**UI rendering**: `GenericToolRenderer` shows the capitalized intent
string
as the primary label when available, falling back to the raw tool name.
### Changes
- Database: `model_intent` boolean column on `mcp_server_configs`
- SDK: `ModelIntent` field on config/create/update types
- API: pass-through in create/update handlers + converter
- mcpclient: schema wrapping in `Info()`, input unwrapping in `Run()`
- Frontend: extraction from streaming + persisted args
- UI: intent label in `GenericToolRenderer`, toggle in admin panel
- Tests: 6 new tests (schema wrapping, unwrapping, passthrough,
fallback)
### Decision log
- **Option lives on MCPServerConfig, not model config**: Built-in tools
already have rich renderers; only MCP tools benefit from model_intent.
- **No system prompt changes**: The JSON Schema `description` on the
`model_intent` property teaches the model inline.
- **Pointer bool on update request**: Follows existing pattern (`*bool`)
so PATCH requests don't reset the value when omitted.
</details>
This commit is contained in:
Generated
+1
@@ -1706,6 +1706,7 @@ CREATE TABLE mcp_server_configs (
|
||||
updated_by uuid,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
model_intent boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT mcp_server_configs_auth_type_check CHECK ((auth_type = ANY (ARRAY['none'::text, 'oauth2'::text, 'api_key'::text, 'custom_headers'::text]))),
|
||||
CONSTRAINT mcp_server_configs_availability_check CHECK ((availability = ANY (ARRAY['force_on'::text, 'default_on'::text, 'default_off'::text]))),
|
||||
CONSTRAINT mcp_server_configs_transport_check CHECK ((transport = ANY (ARRAY['streamable_http'::text, 'sse'::text])))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE mcp_server_configs DROP COLUMN model_intent;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE mcp_server_configs
|
||||
ADD COLUMN model_intent BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -4488,6 +4488,7 @@ type MCPServerConfig struct {
|
||||
UpdatedBy uuid.NullUUID `db:"updated_by" json:"updated_by"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ModelIntent bool `db:"model_intent" json:"model_intent"`
|
||||
}
|
||||
|
||||
type MCPServerUserToken struct {
|
||||
|
||||
@@ -10786,7 +10786,7 @@ func (q *sqlQuerier) DeleteMCPServerUserToken(ctx context.Context, arg DeleteMCP
|
||||
|
||||
const getEnabledMCPServerConfigs = `-- name: GetEnabledMCPServerConfigs :many
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent
|
||||
FROM
|
||||
mcp_server_configs
|
||||
WHERE
|
||||
@@ -10832,6 +10832,7 @@ func (q *sqlQuerier) GetEnabledMCPServerConfigs(ctx context.Context) ([]MCPServe
|
||||
&i.UpdatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -10848,7 +10849,7 @@ func (q *sqlQuerier) GetEnabledMCPServerConfigs(ctx context.Context) ([]MCPServe
|
||||
|
||||
const getForcedMCPServerConfigs = `-- name: GetForcedMCPServerConfigs :many
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent
|
||||
FROM
|
||||
mcp_server_configs
|
||||
WHERE
|
||||
@@ -10895,6 +10896,7 @@ func (q *sqlQuerier) GetForcedMCPServerConfigs(ctx context.Context) ([]MCPServer
|
||||
&i.UpdatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -10911,7 +10913,7 @@ func (q *sqlQuerier) GetForcedMCPServerConfigs(ctx context.Context) ([]MCPServer
|
||||
|
||||
const getMCPServerConfigByID = `-- name: GetMCPServerConfigByID :one
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent
|
||||
FROM
|
||||
mcp_server_configs
|
||||
WHERE
|
||||
@@ -10949,13 +10951,14 @@ func (q *sqlQuerier) GetMCPServerConfigByID(ctx context.Context, id uuid.UUID) (
|
||||
&i.UpdatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getMCPServerConfigBySlug = `-- name: GetMCPServerConfigBySlug :one
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent
|
||||
FROM
|
||||
mcp_server_configs
|
||||
WHERE
|
||||
@@ -10993,13 +10996,14 @@ func (q *sqlQuerier) GetMCPServerConfigBySlug(ctx context.Context, slug string)
|
||||
&i.UpdatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getMCPServerConfigs = `-- name: GetMCPServerConfigs :many
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent
|
||||
FROM
|
||||
mcp_server_configs
|
||||
ORDER BY
|
||||
@@ -11043,6 +11047,7 @@ func (q *sqlQuerier) GetMCPServerConfigs(ctx context.Context) ([]MCPServerConfig
|
||||
&i.UpdatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -11059,7 +11064,7 @@ func (q *sqlQuerier) GetMCPServerConfigs(ctx context.Context) ([]MCPServerConfig
|
||||
|
||||
const getMCPServerConfigsByIDs = `-- name: GetMCPServerConfigsByIDs :many
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent
|
||||
FROM
|
||||
mcp_server_configs
|
||||
WHERE
|
||||
@@ -11105,6 +11110,7 @@ func (q *sqlQuerier) GetMCPServerConfigsByIDs(ctx context.Context, ids []uuid.UU
|
||||
&i.UpdatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -11221,6 +11227,7 @@ INSERT INTO mcp_server_configs (
|
||||
tool_deny_list,
|
||||
availability,
|
||||
enabled,
|
||||
model_intent,
|
||||
created_by,
|
||||
updated_by
|
||||
) VALUES (
|
||||
@@ -11246,11 +11253,12 @@ INSERT INTO mcp_server_configs (
|
||||
$20::text[],
|
||||
$21::text,
|
||||
$22::boolean,
|
||||
$23::uuid,
|
||||
$24::uuid
|
||||
$23::boolean,
|
||||
$24::uuid,
|
||||
$25::uuid
|
||||
)
|
||||
RETURNING
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent
|
||||
`
|
||||
|
||||
type InsertMCPServerConfigParams struct {
|
||||
@@ -11276,6 +11284,7 @@ type InsertMCPServerConfigParams struct {
|
||||
ToolDenyList []string `db:"tool_deny_list" json:"tool_deny_list"`
|
||||
Availability string `db:"availability" json:"availability"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
ModelIntent bool `db:"model_intent" json:"model_intent"`
|
||||
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
|
||||
UpdatedBy uuid.UUID `db:"updated_by" json:"updated_by"`
|
||||
}
|
||||
@@ -11304,6 +11313,7 @@ func (q *sqlQuerier) InsertMCPServerConfig(ctx context.Context, arg InsertMCPSer
|
||||
pq.Array(arg.ToolDenyList),
|
||||
arg.Availability,
|
||||
arg.Enabled,
|
||||
arg.ModelIntent,
|
||||
arg.CreatedBy,
|
||||
arg.UpdatedBy,
|
||||
)
|
||||
@@ -11336,6 +11346,7 @@ func (q *sqlQuerier) InsertMCPServerConfig(ctx context.Context, arg InsertMCPSer
|
||||
&i.UpdatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -11366,12 +11377,13 @@ SET
|
||||
tool_deny_list = $20::text[],
|
||||
availability = $21::text,
|
||||
enabled = $22::boolean,
|
||||
updated_by = $23::uuid,
|
||||
model_intent = $23::boolean,
|
||||
updated_by = $24::uuid,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $24::uuid
|
||||
id = $25::uuid
|
||||
RETURNING
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent
|
||||
`
|
||||
|
||||
type UpdateMCPServerConfigParams struct {
|
||||
@@ -11397,6 +11409,7 @@ type UpdateMCPServerConfigParams struct {
|
||||
ToolDenyList []string `db:"tool_deny_list" json:"tool_deny_list"`
|
||||
Availability string `db:"availability" json:"availability"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
ModelIntent bool `db:"model_intent" json:"model_intent"`
|
||||
UpdatedBy uuid.UUID `db:"updated_by" json:"updated_by"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
@@ -11425,6 +11438,7 @@ func (q *sqlQuerier) UpdateMCPServerConfig(ctx context.Context, arg UpdateMCPSer
|
||||
pq.Array(arg.ToolDenyList),
|
||||
arg.Availability,
|
||||
arg.Enabled,
|
||||
arg.ModelIntent,
|
||||
arg.UpdatedBy,
|
||||
arg.ID,
|
||||
)
|
||||
@@ -11457,6 +11471,7 @@ func (q *sqlQuerier) UpdateMCPServerConfig(ctx context.Context, arg UpdateMCPSer
|
||||
&i.UpdatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ INSERT INTO mcp_server_configs (
|
||||
tool_deny_list,
|
||||
availability,
|
||||
enabled,
|
||||
model_intent,
|
||||
created_by,
|
||||
updated_by
|
||||
) VALUES (
|
||||
@@ -102,6 +103,7 @@ INSERT INTO mcp_server_configs (
|
||||
@tool_deny_list::text[],
|
||||
@availability::text,
|
||||
@enabled::boolean,
|
||||
@model_intent::boolean,
|
||||
@created_by::uuid,
|
||||
@updated_by::uuid
|
||||
)
|
||||
@@ -134,6 +136,7 @@ SET
|
||||
tool_deny_list = @tool_deny_list::text[],
|
||||
availability = @availability::text,
|
||||
enabled = @enabled::boolean,
|
||||
model_intent = @model_intent::boolean,
|
||||
updated_by = @updated_by::uuid,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
|
||||
+13
-3
@@ -170,6 +170,7 @@ func (api *API) createMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
ToolDenyList: coalesceStringSlice(trimStringSlice(req.ToolDenyList)),
|
||||
Availability: strings.TrimSpace(req.Availability),
|
||||
Enabled: req.Enabled,
|
||||
ModelIntent: req.ModelIntent,
|
||||
CreatedBy: apiKey.UserID,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
})
|
||||
@@ -256,6 +257,7 @@ func (api *API) createMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
ToolDenyList: inserted.ToolDenyList,
|
||||
Availability: inserted.Availability,
|
||||
Enabled: inserted.Enabled,
|
||||
ModelIntent: inserted.ModelIntent,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -323,6 +325,7 @@ func (api *API) createMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
ToolDenyList: coalesceStringSlice(trimStringSlice(req.ToolDenyList)),
|
||||
Availability: strings.TrimSpace(req.Availability),
|
||||
Enabled: req.Enabled,
|
||||
ModelIntent: req.ModelIntent,
|
||||
CreatedBy: apiKey.UserID,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
})
|
||||
@@ -572,6 +575,11 @@ func (api *API) updateMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
enabled = *req.Enabled
|
||||
}
|
||||
|
||||
modelIntent := existing.ModelIntent
|
||||
if req.ModelIntent != nil {
|
||||
modelIntent = *req.ModelIntent
|
||||
}
|
||||
|
||||
// When auth_type changes, clear fields belonging to the
|
||||
// previous auth type so stale secrets don't persist.
|
||||
if authType != existing.AuthType {
|
||||
@@ -639,6 +647,7 @@ func (api *API) updateMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
ToolDenyList: toolDenyList,
|
||||
Availability: availability,
|
||||
Enabled: enabled,
|
||||
ModelIntent: modelIntent,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
ID: existing.ID,
|
||||
})
|
||||
@@ -1120,9 +1129,10 @@ func convertMCPServerConfig(config database.MCPServerConfig) codersdk.MCPServerC
|
||||
|
||||
Availability: config.Availability,
|
||||
|
||||
Enabled: config.Enabled,
|
||||
CreatedAt: config.CreatedAt,
|
||||
UpdatedAt: config.UpdatedAt,
|
||||
Enabled: config.Enabled,
|
||||
ModelIntent: config.ModelIntent,
|
||||
CreatedAt: config.CreatedAt,
|
||||
UpdatedAt: config.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ func connectOne(
|
||||
}
|
||||
|
||||
tools = append(
|
||||
tools, newMCPTool(cfg.ID, cfg.Slug, mcpTool, mcpClient),
|
||||
tools, newMCPTool(cfg.ID, cfg.Slug, mcpTool, mcpClient, cfg.ModelIntent),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -401,6 +401,7 @@ type mcpToolWrapper struct {
|
||||
description string
|
||||
parameters map[string]any
|
||||
required []string
|
||||
modelIntent bool
|
||||
client *client.Client
|
||||
providerOptions fantasy.ProviderOptions
|
||||
}
|
||||
@@ -418,6 +419,7 @@ func newMCPTool(
|
||||
serverSlug string,
|
||||
tool mcp.Tool,
|
||||
mcpClient *client.Client,
|
||||
modelIntent bool,
|
||||
) *mcpToolWrapper {
|
||||
return &mcpToolWrapper{
|
||||
configID: configID,
|
||||
@@ -426,22 +428,53 @@ func newMCPTool(
|
||||
description: tool.Description,
|
||||
parameters: tool.InputSchema.Properties,
|
||||
required: tool.InputSchema.Required,
|
||||
modelIntent: modelIntent,
|
||||
client: mcpClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *mcpToolWrapper) Info() fantasy.ToolInfo {
|
||||
// Ensure Required is never nil so that it serializes to [] instead
|
||||
// of null. OpenAI rejects null for the JSON Schema "required" field.
|
||||
required := t.required
|
||||
if required == nil {
|
||||
required = []string{}
|
||||
}
|
||||
|
||||
if !t.modelIntent {
|
||||
return fantasy.ToolInfo{
|
||||
Name: t.prefixedName,
|
||||
Description: t.description,
|
||||
Parameters: t.parameters,
|
||||
Required: required,
|
||||
Parallel: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap original parameters under "properties" and add
|
||||
// "model_intent" so the LLM provides a human-readable
|
||||
// description of each tool call.
|
||||
wrapped := map[string]any{
|
||||
"model_intent": map[string]any{
|
||||
"type": "string",
|
||||
"description": "A short, natural-language, present-participle " +
|
||||
"phrase describing why you are calling this tool. " +
|
||||
"This is shown to the user as a status label while " +
|
||||
"the tool runs. Use plain English with no underscores " +
|
||||
"or technical jargon. Keep it under 100 characters. " +
|
||||
"Good examples: \"Reading the authentication module\", " +
|
||||
"\"Searching for configuration files\", " +
|
||||
"\"Creating a new workspace\".",
|
||||
},
|
||||
"properties": map[string]any{
|
||||
"type": "object",
|
||||
"properties": t.parameters,
|
||||
"required": required,
|
||||
},
|
||||
}
|
||||
return fantasy.ToolInfo{
|
||||
Name: t.prefixedName,
|
||||
Description: t.description,
|
||||
Parameters: t.parameters,
|
||||
Required: required,
|
||||
Parameters: wrapped,
|
||||
Required: []string{"model_intent", "properties"},
|
||||
Parallel: true,
|
||||
}
|
||||
}
|
||||
@@ -450,10 +483,15 @@ func (t *mcpToolWrapper) Run(
|
||||
ctx context.Context,
|
||||
params fantasy.ToolCall,
|
||||
) (fantasy.ToolResponse, error) {
|
||||
input := params.Input
|
||||
if t.modelIntent {
|
||||
input = unwrapModelIntent(input)
|
||||
}
|
||||
|
||||
var args map[string]any
|
||||
if params.Input != "" {
|
||||
if input != "" {
|
||||
if err := json.Unmarshal(
|
||||
[]byte(params.Input), &args,
|
||||
[]byte(input), &args,
|
||||
); err != nil {
|
||||
return fantasy.NewTextErrorResponse(
|
||||
"invalid JSON input: " + err.Error(),
|
||||
@@ -490,6 +528,36 @@ func (t *mcpToolWrapper) SetProviderOptions(
|
||||
t.providerOptions = opts
|
||||
}
|
||||
|
||||
// unwrapModelIntent strips the model_intent wrapper from tool
|
||||
// call input so the remote MCP server receives only the original
|
||||
// arguments. It handles three shapes the model may produce:
|
||||
//
|
||||
// 1. { model_intent, properties: {...} } — correct format
|
||||
// 2. { model_intent, key: val, ... } — flat, no properties wrapper
|
||||
// 3. Anything else — returned as-is
|
||||
func unwrapModelIntent(input string) string {
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal([]byte(input), &parsed); err != nil {
|
||||
return input
|
||||
}
|
||||
|
||||
delete(parsed, "model_intent")
|
||||
|
||||
// Case 1: correct { model_intent, properties: {...} } format.
|
||||
if props, ok := parsed["properties"]; ok {
|
||||
if b, err := json.Marshal(props); err == nil {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: flat { model_intent, key: val, ... } without wrapper.
|
||||
if b, err := json.Marshal(parsed); err == nil {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
// convertCallResult translates an MCP CallToolResult into a
|
||||
// fantasy.ToolResponse. The fantasy response model supports a
|
||||
// single content type per response, so we prioritize text. All
|
||||
|
||||
@@ -986,3 +986,162 @@ func TestConnectAll_CallToolError(t *testing.T) {
|
||||
assert.True(t, resp.IsError, "response should be flagged as error")
|
||||
assert.Contains(t, resp.Content, "something broke")
|
||||
}
|
||||
|
||||
func TestModelIntent_Info_WrapsSchema(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts := newTestMCPServer(t, echoTool())
|
||||
|
||||
cfg := makeConfig("intent-srv", ts.URL)
|
||||
cfg.ModelIntent = true
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
info := tools[0].Info()
|
||||
|
||||
// Top-level schema should have model_intent and properties.
|
||||
_, hasModelIntent := info.Parameters["model_intent"]
|
||||
_, hasProperties := info.Parameters["properties"]
|
||||
assert.True(t, hasModelIntent, "schema should contain model_intent")
|
||||
assert.True(t, hasProperties, "schema should contain properties")
|
||||
|
||||
// Required should include both.
|
||||
assert.Contains(t, info.Required, "model_intent")
|
||||
assert.Contains(t, info.Required, "properties")
|
||||
|
||||
// The original "input" parameter should be nested under
|
||||
// properties.properties.
|
||||
propsObj, ok := info.Parameters["properties"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
innerProps, ok := propsObj["properties"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
_, hasInput := innerProps["input"]
|
||||
assert.True(t, hasInput, "original 'input' param should be nested")
|
||||
}
|
||||
|
||||
func TestModelIntent_Info_NoWrapWhenDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts := newTestMCPServer(t, echoTool())
|
||||
|
||||
cfg := makeConfig("no-intent", ts.URL)
|
||||
cfg.ModelIntent = false
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
info := tools[0].Info()
|
||||
|
||||
// Original schema should be flat — no model_intent wrapper.
|
||||
_, hasModelIntent := info.Parameters["model_intent"]
|
||||
assert.False(t, hasModelIntent, "schema should NOT contain model_intent")
|
||||
_, hasInput := info.Parameters["input"]
|
||||
assert.True(t, hasInput, "original 'input' param should be at top level")
|
||||
}
|
||||
|
||||
func TestModelIntent_Run_UnwrapsProperties(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts := newTestMCPServer(t, echoTool())
|
||||
|
||||
cfg := makeConfig("unwrap-srv", ts.URL)
|
||||
cfg.ModelIntent = true
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
// Correct format: model_intent + properties wrapper.
|
||||
resp, err := tools[0].Run(ctx, fantasy.ToolCall{
|
||||
ID: "call-1",
|
||||
Name: "unwrap-srv__echo",
|
||||
Input: `{"model_intent":"Testing echo","properties":{"input":"hello"}}`,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.False(t, resp.IsError)
|
||||
assert.Equal(t, "echo: hello", resp.Content)
|
||||
}
|
||||
|
||||
func TestModelIntent_Run_UnwrapsFlat(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts := newTestMCPServer(t, echoTool())
|
||||
|
||||
cfg := makeConfig("flat-srv", ts.URL)
|
||||
cfg.ModelIntent = true
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
// Flat format: model_intent at top level, no properties wrapper.
|
||||
resp, err := tools[0].Run(ctx, fantasy.ToolCall{
|
||||
ID: "call-2",
|
||||
Name: "flat-srv__echo",
|
||||
Input: `{"model_intent":"Testing flat","input":"world"}`,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.False(t, resp.IsError)
|
||||
assert.Equal(t, "echo: world", resp.Content)
|
||||
}
|
||||
|
||||
func TestModelIntent_Run_PassthroughWhenDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts := newTestMCPServer(t, echoTool())
|
||||
|
||||
cfg := makeConfig("pass-srv", ts.URL)
|
||||
cfg.ModelIntent = false
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
// Without model_intent, input is passed through unchanged.
|
||||
resp, err := tools[0].Run(ctx, fantasy.ToolCall{
|
||||
ID: "call-3",
|
||||
Name: "pass-srv__echo",
|
||||
Input: `{"input":"direct"}`,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.False(t, resp.IsError)
|
||||
assert.Equal(t, "echo: direct", resp.Content)
|
||||
}
|
||||
|
||||
func TestModelIntent_Run_FallbackOnBadJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts := newTestMCPServer(t, echoTool())
|
||||
|
||||
cfg := makeConfig("bad-srv", ts.URL)
|
||||
cfg.ModelIntent = true
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
// Malformed JSON should not panic — the error is returned
|
||||
// from the JSON unmarshal in Run(), not from unwrap.
|
||||
resp, err := tools[0].Run(ctx, fantasy.ToolCall{
|
||||
ID: "call-bad",
|
||||
Name: "bad-srv__echo",
|
||||
Input: `not-json`,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, resp.IsError, "malformed input should produce an error response")
|
||||
}
|
||||
|
||||
+6
-3
@@ -64,9 +64,10 @@ type MCPServerConfig struct {
|
||||
// Availability policy set by admin.
|
||||
Availability string `json:"availability"` // "force_on", "default_on", "default_off"
|
||||
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ModelIntent bool `json:"model_intent"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
|
||||
// Per-user state (populated for non-admin requests).
|
||||
AuthConnected bool `json:"auth_connected"`
|
||||
@@ -97,6 +98,7 @@ type CreateMCPServerConfigRequest struct {
|
||||
|
||||
Availability string `json:"availability" validate:"required,oneof=force_on default_on default_off"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ModelIntent bool `json:"model_intent"`
|
||||
}
|
||||
|
||||
// UpdateMCPServerConfigRequest is the request to update an MCP server config.
|
||||
@@ -124,6 +126,7 @@ type UpdateMCPServerConfigRequest struct {
|
||||
|
||||
Availability *string `json:"availability,omitempty" validate:"omitempty,oneof=force_on default_on default_off"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
ModelIntent *bool `json:"model_intent,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) MCPServerConfigs(ctx context.Context) ([]MCPServerConfig, error) {
|
||||
|
||||
Generated
+3
@@ -2406,6 +2406,7 @@ export interface CreateMCPServerConfigRequest {
|
||||
readonly tool_deny_list?: readonly string[];
|
||||
readonly availability: string;
|
||||
readonly enabled: boolean;
|
||||
readonly model_intent: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/organizations.go
|
||||
@@ -4001,6 +4002,7 @@ export interface MCPServerConfig {
|
||||
*/
|
||||
readonly availability: string; // "force_on", "default_on", "default_off"
|
||||
readonly enabled: boolean;
|
||||
readonly model_intent: boolean;
|
||||
readonly created_at: string;
|
||||
readonly updated_at: string;
|
||||
/**
|
||||
@@ -7232,6 +7234,7 @@ export interface UpdateMCPServerConfigRequest {
|
||||
readonly tool_deny_list?: string[];
|
||||
readonly availability?: string;
|
||||
readonly enabled?: boolean;
|
||||
readonly model_intent?: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/notifications.go
|
||||
|
||||
@@ -491,6 +491,7 @@ const sampleMCPServers = [
|
||||
tool_deny_list: [],
|
||||
availability: "default_on",
|
||||
enabled: true,
|
||||
model_intent: false,
|
||||
auth_connected: true,
|
||||
created_at: "2025-01-01T00:00:00Z",
|
||||
updated_at: "2025-01-01T00:00:00Z",
|
||||
@@ -650,6 +651,53 @@ export const MCPToolNoServer: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const MCPToolModelIntentRunning: Story = {
|
||||
args: {
|
||||
name: "linear__list_issues",
|
||||
status: "running",
|
||||
args: { project: "backend" },
|
||||
modelIntent: "Fetching backend issues from Linear",
|
||||
mcpServerConfigId: "mcp-server-1",
|
||||
mcpServers: sampleMCPServers,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
// Should show the model intent label instead of the raw tool name.
|
||||
expect(
|
||||
canvas.getByText("Fetching backend issues from Linear"),
|
||||
).toBeInTheDocument();
|
||||
// Spinner should be visible while running.
|
||||
expect(canvasElement.querySelector(".animate-spin")).not.toBeNull();
|
||||
},
|
||||
};
|
||||
|
||||
export const MCPToolModelIntentCompleted: Story = {
|
||||
args: {
|
||||
name: "github__create_pull_request",
|
||||
status: "completed",
|
||||
args: { title: "Fix auth flow", base: "main" },
|
||||
result: { url: "https://github.com/org/repo/pull/42" },
|
||||
modelIntent: "creating pull request for auth fix",
|
||||
mcpServerConfigId: "mcp-server-1",
|
||||
mcpServers: [
|
||||
{
|
||||
...sampleMCPServers[0],
|
||||
slug: "github",
|
||||
display_name: "GitHub",
|
||||
icon_url:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg",
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
// Intent should be capitalized.
|
||||
expect(
|
||||
canvas.getByText("Creating pull request for auth fix"),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WriteFile stories
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -69,6 +69,8 @@ interface ToolProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
|
||||
mcpServerConfigId?: string;
|
||||
/** Available MCP server configs for icon/name lookup. */
|
||||
mcpServers?: readonly TypesGen.MCPServerConfig[];
|
||||
/** Human-readable intent extracted from the model's tool-call args. */
|
||||
modelIntent?: string;
|
||||
}
|
||||
|
||||
// Props passed to each tool-specific renderer function. Each renderer
|
||||
@@ -85,6 +87,7 @@ type ToolRendererProps = {
|
||||
subagentStatusOverrides?: Map<string, string>;
|
||||
mcpServerConfigId?: string;
|
||||
mcpServers?: readonly TypesGen.MCPServerConfig[];
|
||||
modelIntent?: string;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -497,6 +500,7 @@ const GenericToolRenderer: FC<ToolRendererProps> = ({
|
||||
isError,
|
||||
mcpServerConfigId,
|
||||
mcpServers,
|
||||
modelIntent,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
@@ -534,12 +538,18 @@ const GenericToolRenderer: FC<ToolRendererProps> = ({
|
||||
isRunning={isRunning}
|
||||
serverName={mcpServer?.display_name}
|
||||
/>
|
||||
<ToolLabel
|
||||
name={name}
|
||||
args={args}
|
||||
result={result}
|
||||
mcpSlug={mcpServer?.slug}
|
||||
/>
|
||||
{modelIntent ? (
|
||||
<span className="truncate text-sm text-content-secondary">
|
||||
{modelIntent.charAt(0).toUpperCase() + modelIntent.slice(1)}
|
||||
</span>
|
||||
) : (
|
||||
<ToolLabel
|
||||
name={name}
|
||||
args={args}
|
||||
result={result}
|
||||
mcpSlug={mcpServer?.slug}
|
||||
/>
|
||||
)}
|
||||
{isError && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -646,6 +656,7 @@ export const Tool = memo(
|
||||
subagentStatusOverrides,
|
||||
mcpServerConfigId,
|
||||
mcpServers,
|
||||
modelIntent,
|
||||
ref,
|
||||
...props
|
||||
}: ToolProps) => {
|
||||
@@ -676,6 +687,7 @@ export const Tool = memo(
|
||||
subagentStatusOverrides={subagentStatusOverrides}
|
||||
mcpServerConfigId={mcpServerConfigId}
|
||||
mcpServers={mcpServers}
|
||||
modelIntent={modelIntent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -480,6 +480,7 @@ const makeMCPServer = (
|
||||
tool_deny_list: overrides.tool_deny_list ?? [],
|
||||
availability: overrides.availability ?? "default_on",
|
||||
enabled: overrides.enabled ?? true,
|
||||
model_intent: overrides.model_intent ?? false,
|
||||
created_at: overrides.created_at ?? now,
|
||||
updated_at: overrides.updated_at ?? now,
|
||||
auth_connected: overrides.auth_connected ?? false,
|
||||
|
||||
@@ -367,6 +367,7 @@ const BlockList: FC<{
|
||||
}
|
||||
mcpServerConfigId={tool.mcpServerConfigId}
|
||||
mcpServers={mcpServers}
|
||||
modelIntent={tool.modelIntent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -407,6 +408,7 @@ const BlockList: FC<{
|
||||
}
|
||||
mcpServerConfigId={tool.mcpServerConfigId}
|
||||
mcpServers={mcpServers}
|
||||
modelIntent={tool.modelIntent}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -93,6 +93,12 @@ export const mergeTools = (
|
||||
for (const call of calls) {
|
||||
seen.add(call.id);
|
||||
const result = resultById.get(call.id);
|
||||
// Extract model_intent from the tool call args if present.
|
||||
const callArgs = call.args as Record<string, unknown> | undefined;
|
||||
const modelIntent =
|
||||
typeof callArgs?.model_intent === "string"
|
||||
? callArgs.model_intent
|
||||
: undefined;
|
||||
merged.push({
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
@@ -101,6 +107,7 @@ export const mergeTools = (
|
||||
isError: result?.isError ?? false,
|
||||
status: result ? (result.isError ? "error" : "completed") : "completed",
|
||||
mcpServerConfigId: call.mcpServerConfigId || result?.mcpServerConfigId,
|
||||
modelIntent,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,13 @@ export const applyMessagePartToStreamState = (
|
||||
part.args_delta,
|
||||
);
|
||||
|
||||
// Extract model_intent from the incrementally parsed args.
|
||||
const merged = nextArgs.value as Record<string, unknown> | undefined;
|
||||
const modelIntent =
|
||||
typeof merged?.model_intent === "string"
|
||||
? merged.model_intent
|
||||
: existing?.modelIntent;
|
||||
|
||||
return {
|
||||
...nextState,
|
||||
blocks: ensureToolBlock(nextState.blocks, toolCallID),
|
||||
@@ -72,6 +79,7 @@ export const applyMessagePartToStreamState = (
|
||||
argsRaw: nextArgs.rawText,
|
||||
mcpServerConfigId:
|
||||
part.mcp_server_config_id || existing?.mcpServerConfigId,
|
||||
modelIntent,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -201,6 +209,7 @@ export const buildStreamTools = (
|
||||
isError: result?.isError ?? false,
|
||||
status: result ? (result.isError ? "error" : "completed") : "running",
|
||||
mcpServerConfigId: call.mcpServerConfigId || result?.mcpServerConfigId,
|
||||
modelIntent: call.modelIntent,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ export type MergedTool = {
|
||||
isError: boolean;
|
||||
status: "completed" | "error" | "running";
|
||||
mcpServerConfigId?: string;
|
||||
modelIntent?: string;
|
||||
};
|
||||
|
||||
export type RenderBlock =
|
||||
@@ -79,6 +80,7 @@ type StreamToolCall = {
|
||||
args?: unknown;
|
||||
argsRaw?: string;
|
||||
mcpServerConfigId?: string;
|
||||
modelIntent?: string;
|
||||
};
|
||||
|
||||
type StreamToolResult = {
|
||||
|
||||
@@ -33,6 +33,7 @@ const createServerConfig = (
|
||||
tool_deny_list: overrides.tool_deny_list ?? [],
|
||||
availability: overrides.availability ?? "default_on",
|
||||
enabled: overrides.enabled ?? true,
|
||||
model_intent: overrides.model_intent ?? false,
|
||||
created_at: overrides.created_at ?? now,
|
||||
updated_at: overrides.updated_at ?? now,
|
||||
auth_connected: overrides.auth_connected ?? false,
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Button } from "#/components/Button/Button";
|
||||
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
import { IconField } from "#/components/IconField/IconField";
|
||||
import { Input } from "#/components/Input/Input";
|
||||
import { Label } from "#/components/Label/Label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -231,6 +232,7 @@ interface MCPServerFormValues {
|
||||
apiKeyTouched: boolean;
|
||||
availability: string;
|
||||
enabled: boolean;
|
||||
modelIntent: boolean;
|
||||
toolAllowList: string;
|
||||
toolDenyList: string;
|
||||
customHeaders: Array<{ key: string; value: string }>;
|
||||
@@ -259,6 +261,7 @@ const buildInitialValues = (
|
||||
apiKeyTouched: false,
|
||||
availability: server?.availability ?? "default_off",
|
||||
enabled: server?.enabled ?? true,
|
||||
modelIntent: server?.model_intent ?? false,
|
||||
toolAllowList: joinList(server?.tool_allow_list),
|
||||
toolDenyList: joinList(server?.tool_deny_list),
|
||||
customHeaders: [],
|
||||
@@ -312,6 +315,7 @@ const ServerForm: FC<ServerFormProps> = ({
|
||||
auth_type: values.authType,
|
||||
availability: values.availability,
|
||||
enabled: values.enabled,
|
||||
model_intent: values.modelIntent,
|
||||
...(values.authType === "oauth2" && {
|
||||
oauth2_client_id: values.oauth2ClientID.trim(),
|
||||
oauth2_client_secret: effectiveOAuth2Secret,
|
||||
@@ -771,7 +775,25 @@ const ServerForm: FC<ServerFormProps> = ({
|
||||
</Field>{" "}
|
||||
{/* ── Tool governance row ── */}
|
||||
<hr className="!my-2 border-0 border-t border-solid border-border" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label htmlFor={`${formId}-model-intent`}>Model intent</Label>
|
||||
<p className="text-sm text-content-secondary">
|
||||
Require the model to describe each tool call's purpose in
|
||||
natural language, shown as a status label in the UI.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id={`${formId}-model-intent`}
|
||||
checked={form.values.modelIntent}
|
||||
onCheckedChange={(v) => {
|
||||
form.setFieldValue("modelIntent", v);
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid items-start gap-5 sm:grid-cols-2">
|
||||
{" "}
|
||||
<Field
|
||||
label="Tool Allow List"
|
||||
htmlFor={`${formId}-allow-list`}
|
||||
@@ -785,7 +807,6 @@ const ServerForm: FC<ServerFormProps> = ({
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Tool Deny List"
|
||||
htmlFor={`${formId}-deny-list`}
|
||||
|
||||
@@ -31,6 +31,7 @@ const createServerConfig = (
|
||||
tool_deny_list: overrides.tool_deny_list ?? [],
|
||||
availability: overrides.availability ?? "default_on",
|
||||
enabled: overrides.enabled ?? true,
|
||||
model_intent: overrides.model_intent ?? false,
|
||||
created_at: overrides.created_at ?? now,
|
||||
updated_at: overrides.updated_at ?? now,
|
||||
auth_connected: overrides.auth_connected ?? false,
|
||||
|
||||
Reference in New Issue
Block a user