mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
d973a709df
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>
217 lines
5.0 KiB
SQL
217 lines
5.0 KiB
SQL
-- name: GetMCPServerConfigByID :one
|
|
SELECT
|
|
*
|
|
FROM
|
|
mcp_server_configs
|
|
WHERE
|
|
id = @id::uuid;
|
|
|
|
-- name: GetMCPServerConfigBySlug :one
|
|
SELECT
|
|
*
|
|
FROM
|
|
mcp_server_configs
|
|
WHERE
|
|
slug = @slug::text;
|
|
|
|
-- name: GetMCPServerConfigs :many
|
|
SELECT
|
|
*
|
|
FROM
|
|
mcp_server_configs
|
|
ORDER BY
|
|
display_name ASC;
|
|
|
|
-- name: GetEnabledMCPServerConfigs :many
|
|
SELECT
|
|
*
|
|
FROM
|
|
mcp_server_configs
|
|
WHERE
|
|
enabled = TRUE
|
|
ORDER BY
|
|
display_name ASC;
|
|
|
|
-- name: GetMCPServerConfigsByIDs :many
|
|
SELECT
|
|
*
|
|
FROM
|
|
mcp_server_configs
|
|
WHERE
|
|
id = ANY(@ids::uuid[])
|
|
ORDER BY
|
|
display_name ASC;
|
|
|
|
-- name: GetForcedMCPServerConfigs :many
|
|
SELECT
|
|
*
|
|
FROM
|
|
mcp_server_configs
|
|
WHERE
|
|
enabled = TRUE
|
|
AND availability = 'force_on'
|
|
ORDER BY
|
|
display_name ASC;
|
|
|
|
-- name: InsertMCPServerConfig :one
|
|
INSERT INTO mcp_server_configs (
|
|
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,
|
|
model_intent,
|
|
created_by,
|
|
updated_by
|
|
) VALUES (
|
|
@display_name::text,
|
|
@slug::text,
|
|
@description::text,
|
|
@icon_url::text,
|
|
@transport::text,
|
|
@url::text,
|
|
@auth_type::text,
|
|
@oauth2_client_id::text,
|
|
@oauth2_client_secret::text,
|
|
sqlc.narg('oauth2_client_secret_key_id')::text,
|
|
@oauth2_auth_url::text,
|
|
@oauth2_token_url::text,
|
|
@oauth2_scopes::text,
|
|
@api_key_header::text,
|
|
@api_key_value::text,
|
|
sqlc.narg('api_key_value_key_id')::text,
|
|
@custom_headers::text,
|
|
sqlc.narg('custom_headers_key_id')::text,
|
|
@tool_allow_list::text[],
|
|
@tool_deny_list::text[],
|
|
@availability::text,
|
|
@enabled::boolean,
|
|
@model_intent::boolean,
|
|
@created_by::uuid,
|
|
@updated_by::uuid
|
|
)
|
|
RETURNING
|
|
*;
|
|
|
|
-- name: UpdateMCPServerConfig :one
|
|
UPDATE
|
|
mcp_server_configs
|
|
SET
|
|
display_name = @display_name::text,
|
|
slug = @slug::text,
|
|
description = @description::text,
|
|
icon_url = @icon_url::text,
|
|
transport = @transport::text,
|
|
url = @url::text,
|
|
auth_type = @auth_type::text,
|
|
oauth2_client_id = @oauth2_client_id::text,
|
|
oauth2_client_secret = @oauth2_client_secret::text,
|
|
oauth2_client_secret_key_id = sqlc.narg('oauth2_client_secret_key_id')::text,
|
|
oauth2_auth_url = @oauth2_auth_url::text,
|
|
oauth2_token_url = @oauth2_token_url::text,
|
|
oauth2_scopes = @oauth2_scopes::text,
|
|
api_key_header = @api_key_header::text,
|
|
api_key_value = @api_key_value::text,
|
|
api_key_value_key_id = sqlc.narg('api_key_value_key_id')::text,
|
|
custom_headers = @custom_headers::text,
|
|
custom_headers_key_id = sqlc.narg('custom_headers_key_id')::text,
|
|
tool_allow_list = @tool_allow_list::text[],
|
|
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
|
|
id = @id::uuid
|
|
RETURNING
|
|
*;
|
|
|
|
-- name: DeleteMCPServerConfigByID :exec
|
|
DELETE FROM
|
|
mcp_server_configs
|
|
WHERE
|
|
id = @id::uuid;
|
|
|
|
-- name: GetMCPServerUserToken :one
|
|
SELECT
|
|
*
|
|
FROM
|
|
mcp_server_user_tokens
|
|
WHERE
|
|
mcp_server_config_id = @mcp_server_config_id::uuid
|
|
AND user_id = @user_id::uuid;
|
|
|
|
-- name: GetMCPServerUserTokensByUserID :many
|
|
SELECT
|
|
*
|
|
FROM
|
|
mcp_server_user_tokens
|
|
WHERE
|
|
user_id = @user_id::uuid;
|
|
|
|
-- name: UpsertMCPServerUserToken :one
|
|
INSERT INTO mcp_server_user_tokens (
|
|
mcp_server_config_id,
|
|
user_id,
|
|
access_token,
|
|
access_token_key_id,
|
|
refresh_token,
|
|
refresh_token_key_id,
|
|
token_type,
|
|
expiry
|
|
) VALUES (
|
|
@mcp_server_config_id::uuid,
|
|
@user_id::uuid,
|
|
@access_token::text,
|
|
sqlc.narg('access_token_key_id')::text,
|
|
@refresh_token::text,
|
|
sqlc.narg('refresh_token_key_id')::text,
|
|
@token_type::text,
|
|
sqlc.narg('expiry')::timestamptz
|
|
)
|
|
ON CONFLICT (mcp_server_config_id, user_id) DO UPDATE SET
|
|
access_token = @access_token::text,
|
|
access_token_key_id = sqlc.narg('access_token_key_id')::text,
|
|
refresh_token = @refresh_token::text,
|
|
refresh_token_key_id = sqlc.narg('refresh_token_key_id')::text,
|
|
token_type = @token_type::text,
|
|
expiry = sqlc.narg('expiry')::timestamptz,
|
|
updated_at = NOW()
|
|
RETURNING
|
|
*;
|
|
|
|
-- name: DeleteMCPServerUserToken :exec
|
|
DELETE FROM
|
|
mcp_server_user_tokens
|
|
WHERE
|
|
mcp_server_config_id = @mcp_server_config_id::uuid
|
|
AND user_id = @user_id::uuid;
|
|
|
|
-- name: CleanupDeletedMCPServerIDsFromChats :exec
|
|
UPDATE chats
|
|
SET mcp_server_ids = (
|
|
SELECT COALESCE(array_agg(sid), '{}')
|
|
FROM unnest(chats.mcp_server_ids) AS sid
|
|
WHERE sid IN (SELECT id FROM mcp_server_configs)
|
|
)
|
|
WHERE mcp_server_ids != '{}'
|
|
AND NOT (mcp_server_ids <@ COALESCE((SELECT array_agg(id) FROM mcp_server_configs), '{}'));
|