Files
gitmost/apps/client/src/features/workspace/services/ai-mcp-server-service.ts
claude code agent 227 77ccc596ea 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>
2026-06-25 11:36:01 +03:00

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;
}