feat(ai-chat): per-MCP-server instructions in the agent system prompt (#180)
Admins can now give each EXTERNAL MCP server a free-text instruction ("how/
when to use this server's tools") that the agent receives in its SYSTEM
PROMPT next to the tool descriptions — porting the built-in SERVER_INSTRUCTIONS
idea to admin-configured servers. Trusted, admin-authored text (like a system
prompt); NON-secret, so unlike headersEnc it IS returned in views/forms.
- Migration: nullable `instructions text` on ai_mcp_servers (old rows = null =
no guidance). Table type + repo insert/update (blank/whitespace -> null via
blankToNull). DTO `@MaxLength(4000)`. Service threads it through
McpServerView/toView.
- mcp-clients: `McpServerInstruction { serverName, toolPrefix, instructions }`
threaded through the toolset/cache/lease. Guidance is built ONLY for a server
that actually connected AND contributed >=1 callable tool (the allowlist may
filter all of them out) AND has non-blank text — so a guide never appears for
tools the agent cannot call. Cached with the toolset, so an edit is picked up
next turn via the existing CRUD cache invalidation.
- System prompt: `buildMcpToolingBlock` renders an <mcp_tooling> block INSIDE
the safety sandwich (after context, before the trailing SAFETY_FRAMEWORK) so
it informs tool choice but cannot override the rules; each section is headed
by the server's `prefix_*` namespace. Empty/blank -> block omitted. The
caller (ai-chat.service) now builds the external toolset BEFORE the prompt and
passes external.instructions; client-handle lifecycle (close-once) unchanged.
- Client: instructions field in types + a Textarea (autosize, maxLength 4000)
in the MCP-server form with a namespace-prefix hint; i18n (en/ru).
Tests across every layer (prompt block placement + both SAFETY copies; view
blank->null; buildEntry includes guidance only for connected+>=1-tool+non-blank;
DTO MaxLength; repo + integration round-trip; service wiring). Delegated impl
reviewed (APPROVE); applied the import-type follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
Switch,
|
||||
TagsInput,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
@@ -35,6 +36,8 @@ const formSchema = z.object({
|
||||
// Write-only secret buffer. Empty string means "do not change" (unless cleared).
|
||||
authHeader: z.string(),
|
||||
toolAllowlist: z.array(z.string()),
|
||||
// Admin-authored prompt guidance (#180). Capped to mirror the DTO MaxLength.
|
||||
instructions: z.string().max(4000),
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
|
||||
@@ -63,6 +66,7 @@ function buildInitialValues(server?: IAiMcpServer): FormValues {
|
||||
toolAllowlist: Array.isArray(server?.toolAllowlist)
|
||||
? server.toolAllowlist
|
||||
: [],
|
||||
instructions: server?.instructions ?? "",
|
||||
enabled: server?.enabled ?? true,
|
||||
};
|
||||
}
|
||||
@@ -124,6 +128,8 @@ export default function AiMcpServerForm({
|
||||
transport: values.transport,
|
||||
url: values.url,
|
||||
toolAllowlist: values.toolAllowlist,
|
||||
// Always sent: a blank value clears the stored guidance (server -> null).
|
||||
instructions: values.instructions,
|
||||
enabled: values.enabled,
|
||||
};
|
||||
// Only attach headers when set or explicitly cleared (omit => unchanged).
|
||||
@@ -135,6 +141,8 @@ export default function AiMcpServerForm({
|
||||
transport: values.transport,
|
||||
url: values.url,
|
||||
toolAllowlist: values.toolAllowlist,
|
||||
// Blank => server stores null (no guidance).
|
||||
instructions: values.instructions,
|
||||
enabled: values.enabled,
|
||||
};
|
||||
// On create, only a typed value matters (no prior stored headers).
|
||||
@@ -158,10 +166,7 @@ export default function AiMcpServerForm({
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t("Server name")}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput label={t("Server name")} {...form.getInputProps("name")} />
|
||||
|
||||
<Select
|
||||
label={t("Transport")}
|
||||
@@ -177,7 +182,7 @@ export default function AiMcpServerForm({
|
||||
// Clarify that the value is sent verbatim as the Authorization header,
|
||||
// so the user supplies the full scheme (no implicit Bearer prefix).
|
||||
description={t(
|
||||
"Sent verbatim as the value of the Authorization header (e.g. \"Bearer <token>\" or \"Basic <base64>\").",
|
||||
'Sent verbatim as the value of the Authorization header (e.g. "Bearer <token>" or "Basic <base64>").',
|
||||
)}
|
||||
// Placeholder hints whether headers are stored; the value is never shown.
|
||||
placeholder={hasHeaders ? t("•••• set") : ""}
|
||||
@@ -208,6 +213,20 @@ export default function AiMcpServerForm({
|
||||
{...form.getInputProps("toolAllowlist")}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label={t("Instructions")}
|
||||
// Hint that the text is injected into the agent's system prompt and that
|
||||
// the server's tools are namespaced under <name>_* (the prompt header).
|
||||
description={t(
|
||||
"Optional guidance for the agent on how and when to use this server's tools. Injected into the system prompt. The server's tools are namespaced as \"<server name>_*\".",
|
||||
)}
|
||||
autosize
|
||||
minRows={2}
|
||||
maxRows={8}
|
||||
maxLength={4000}
|
||||
{...form.getInputProps("instructions")}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={t("Enabled")}
|
||||
checked={form.values.enabled}
|
||||
|
||||
@@ -14,6 +14,9 @@ export interface IAiMcpServer {
|
||||
enabled: boolean;
|
||||
toolAllowlist: string[] | null;
|
||||
hasHeaders: boolean;
|
||||
// Admin-authored guidance injected into the agent system prompt (#180).
|
||||
// NON-secret, so it IS returned. Null when no guidance is configured.
|
||||
instructions: string | null;
|
||||
}
|
||||
|
||||
// Create payload. `headers` is write-only: omit => no auth headers.
|
||||
@@ -25,6 +28,8 @@ export interface IAiMcpServerCreate {
|
||||
// never returned.
|
||||
headers?: Record<string, string>;
|
||||
toolAllowlist?: string[];
|
||||
// Admin-authored prompt guidance (#180). Blank => stored as null.
|
||||
instructions?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -39,6 +44,8 @@ export interface IAiMcpServerUpdate {
|
||||
url?: string;
|
||||
headers?: Record<string, string>;
|
||||
toolAllowlist?: string[];
|
||||
// Admin-authored prompt guidance (#180). Absent => unchanged; blank => cleared.
|
||||
instructions?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user