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>
103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
import api from "@/lib/api-client";
|
|
|
|
// External MCP server transports (mirrors the server's MCP_TRANSPORTS).
|
|
export type McpTransport = "http" | "sse";
|
|
|
|
// Admin-facing view of a configured external MCP server.
|
|
// SECURITY (§8.10): the auth headers are NEVER returned — only `hasHeaders`
|
|
// signals whether any are stored. `toolAllowlist` is null when unrestricted.
|
|
export interface IAiMcpServer {
|
|
id: string;
|
|
name: string;
|
|
transport: McpTransport;
|
|
url: string;
|
|
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.
|
|
export interface IAiMcpServerCreate {
|
|
name: string;
|
|
transport: McpTransport;
|
|
url: string;
|
|
// Auth headers map (e.g. { Authorization: 'Bearer ...' }). Encrypted on save;
|
|
// never returned.
|
|
headers?: Record<string, string>;
|
|
toolAllowlist?: string[];
|
|
// Admin-authored prompt guidance (#180). Blank => stored as null.
|
|
instructions?: string;
|
|
enabled?: boolean;
|
|
}
|
|
|
|
// Update payload. Every field is optional (partial update). `headers` semantics:
|
|
// - omit -> auth headers unchanged
|
|
// - {} (empty) -> auth headers cleared
|
|
// - non-empty value -> auth headers replaced
|
|
export interface IAiMcpServerUpdate {
|
|
id: string;
|
|
name?: string;
|
|
transport?: McpTransport;
|
|
url?: string;
|
|
headers?: Record<string, string>;
|
|
toolAllowlist?: string[];
|
|
// Admin-authored prompt guidance (#180). Absent => unchanged; blank => cleared.
|
|
instructions?: string;
|
|
enabled?: boolean;
|
|
}
|
|
|
|
// Result of a "Test connection" against a SAVED server (by id).
|
|
// The error string is already sanitized server-side; never carries secrets.
|
|
export type IAiMcpServerTestResult =
|
|
| { ok: true; tools: string[] }
|
|
| { ok: false; error: string };
|
|
|
|
export async function getAiMcpServers(): Promise<IAiMcpServer[]> {
|
|
const req = await api.post<IAiMcpServer[]>("/workspace/ai-mcp-servers");
|
|
return req.data;
|
|
}
|
|
|
|
export async function createAiMcpServer(
|
|
data: IAiMcpServerCreate,
|
|
): Promise<IAiMcpServer> {
|
|
const req = await api.post<IAiMcpServer>(
|
|
"/workspace/ai-mcp-servers/create",
|
|
data,
|
|
);
|
|
return req.data;
|
|
}
|
|
|
|
export async function updateAiMcpServer(
|
|
data: IAiMcpServerUpdate,
|
|
): Promise<IAiMcpServer> {
|
|
const req = await api.post<IAiMcpServer>(
|
|
"/workspace/ai-mcp-servers/update",
|
|
data,
|
|
);
|
|
return req.data;
|
|
}
|
|
|
|
export async function deleteAiMcpServer(
|
|
id: string,
|
|
): Promise<{ success: true }> {
|
|
const req = await api.post<{ success: true }>(
|
|
"/workspace/ai-mcp-servers/delete",
|
|
{ id },
|
|
);
|
|
return req.data;
|
|
}
|
|
|
|
// Tests a SAVED server by id (the server connects with the stored headers).
|
|
export async function testAiMcpServer(
|
|
id: string,
|
|
): Promise<IAiMcpServerTestResult> {
|
|
const req = await api.post<IAiMcpServerTestResult>(
|
|
"/workspace/ai-mcp-servers/test",
|
|
{ id },
|
|
);
|
|
return req.data;
|
|
}
|