feat: carry mcp primitives content as a system prompt

This commit is contained in:
Kadxy 2025-01-09 10:09:46 +08:00
parent fe67f79050
commit 77be190d76
6 changed files with 448 additions and 275 deletions

View File

@ -1,17 +1,18 @@
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import React, { import React, {
useState,
useRef,
useEffect,
useMemo,
useCallback,
Fragment, Fragment,
RefObject, RefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react"; } from "react";
import SendWhiteIcon from "../icons/send-white.svg"; import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg"; import BrainIcon from "../icons/brain.svg";
import RenameIcon from "../icons/rename.svg"; import RenameIcon from "../icons/rename.svg";
import EditIcon from "../icons/rename.svg";
import ExportIcon from "../icons/share.svg"; import ExportIcon from "../icons/share.svg";
import ReturnIcon from "../icons/return.svg"; import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg"; import CopyIcon from "../icons/copy.svg";
@ -24,11 +25,11 @@ import MaskIcon from "../icons/mask.svg";
import MaxIcon from "../icons/max.svg"; import MaxIcon from "../icons/max.svg";
import MinIcon from "../icons/min.svg"; import MinIcon from "../icons/min.svg";
import ResetIcon from "../icons/reload.svg"; import ResetIcon from "../icons/reload.svg";
import ReloadIcon from "../icons/reload.svg";
import BreakIcon from "../icons/break.svg"; import BreakIcon from "../icons/break.svg";
import SettingsIcon from "../icons/chat-settings.svg"; import SettingsIcon from "../icons/chat-settings.svg";
import DeleteIcon from "../icons/clear.svg"; import DeleteIcon from "../icons/clear.svg";
import PinIcon from "../icons/pin.svg"; import PinIcon from "../icons/pin.svg";
import EditIcon from "../icons/rename.svg";
import ConfirmIcon from "../icons/confirm.svg"; import ConfirmIcon from "../icons/confirm.svg";
import CloseIcon from "../icons/close.svg"; import CloseIcon from "../icons/close.svg";
import CancelIcon from "../icons/cancel.svg"; import CancelIcon from "../icons/cancel.svg";
@ -45,33 +46,32 @@ import QualityIcon from "../icons/hd.svg";
import StyleIcon from "../icons/palette.svg"; import StyleIcon from "../icons/palette.svg";
import PluginIcon from "../icons/plugin.svg"; import PluginIcon from "../icons/plugin.svg";
import ShortcutkeyIcon from "../icons/shortcutkey.svg"; import ShortcutkeyIcon from "../icons/shortcutkey.svg";
import ReloadIcon from "../icons/reload.svg";
import HeadphoneIcon from "../icons/headphone.svg"; import HeadphoneIcon from "../icons/headphone.svg";
import { import {
ChatMessage,
SubmitKey,
useChatStore,
BOT_HELLO, BOT_HELLO,
ChatMessage,
createMessage, createMessage,
useAccessStore,
Theme,
useAppConfig,
DEFAULT_TOPIC, DEFAULT_TOPIC,
ModelType, ModelType,
SubmitKey,
Theme,
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore, usePluginStore,
} from "../store"; } from "../store";
import { import {
copyToClipboard,
selectOrCopy,
autoGrowTextArea, autoGrowTextArea,
useMobileScreen, copyToClipboard,
getMessageTextContent,
getMessageImages, getMessageImages,
isVisionModel, getMessageTextContent,
isDalle3, isDalle3,
showPlugins, isVisionModel,
safeLocalStorage, safeLocalStorage,
selectOrCopy,
showPlugins,
useMobileScreen,
} from "../utils"; } from "../utils";
import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
@ -79,7 +79,7 @@ import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { ChatControllerPool } from "../client/controller"; import { ChatControllerPool } from "../client/controller";
import { DalleSize, DalleQuality, DalleStyle } from "../typing"; import { DalleQuality, DalleSize, DalleStyle } from "../typing";
import { Prompt, usePromptStore } from "../store/prompt"; import { Prompt, usePromptStore } from "../store/prompt";
import Locale from "../locales"; import Locale from "../locales";
@ -102,8 +102,8 @@ import {
ModelProvider, ModelProvider,
Path, Path,
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
UNFINISHED_INPUT,
ServiceProvider, ServiceProvider,
UNFINISHED_INPUT,
} from "../constant"; } from "../constant";
import { Avatar } from "./emoji"; import { Avatar } from "./emoji";
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
@ -113,9 +113,7 @@ import { prettyObject } from "../utils/format";
import { ExportMessageModal } from "./exporter"; import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { useAllModels } from "../utils/hooks"; import { useAllModels } from "../utils/hooks";
import { MultimodalContent } from "../client/api"; import { ClientApi, MultimodalContent } from "../client/api";
import { ClientApi } from "../client/api";
import { createTTSPlayer } from "../utils/audio"; import { createTTSPlayer } from "../utils/audio";
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts"; import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
@ -427,6 +425,7 @@ function useScrollToBottom(
// for auto-scroll // for auto-scroll
const [autoScroll, setAutoScroll] = useState(true); const [autoScroll, setAutoScroll] = useState(true);
function scrollDomToBottom() { function scrollDomToBottom() {
const dom = scrollRef.current; const dom = scrollRef.current;
if (dom) { if (dom) {
@ -473,6 +472,7 @@ export function ChatActions(props: {
// switch themes // switch themes
const theme = config.theme; const theme = config.theme;
function nextTheme() { function nextTheme() {
const themes = [Theme.Auto, Theme.Light, Theme.Dark]; const themes = [Theme.Auto, Theme.Light, Theme.Dark];
const themeIndex = themes.indexOf(theme); const themeIndex = themes.indexOf(theme);
@ -1237,6 +1237,7 @@ function _Chat() {
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const [speechStatus, setSpeechStatus] = useState(false); const [speechStatus, setSpeechStatus] = useState(false);
const [speechLoading, setSpeechLoading] = useState(false); const [speechLoading, setSpeechLoading] = useState(false);
async function openaiSpeech(text: string) { async function openaiSpeech(text: string) {
if (speechStatus) { if (speechStatus) {
ttsPlayer.stop(); ttsPlayer.stop();
@ -1336,6 +1337,7 @@ function _Chat() {
const [msgRenderIndex, _setMsgRenderIndex] = useState( const [msgRenderIndex, _setMsgRenderIndex] = useState(
Math.max(0, renderMessages.length - CHAT_PAGE_SIZE), Math.max(0, renderMessages.length - CHAT_PAGE_SIZE),
); );
function setMsgRenderIndex(newIndex: number) { function setMsgRenderIndex(newIndex: number) {
newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex); newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex);
newIndex = Math.max(0, newIndex); newIndex = Math.max(0, newIndex);
@ -1371,6 +1373,7 @@ function _Chat() {
setHitBottom(isHitBottom); setHitBottom(isHitBottom);
setAutoScroll(isHitBottom); setAutoScroll(isHitBottom);
}; };
function scrollToBottom() { function scrollToBottom() {
setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE); setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
scrollDomToBottom(); scrollDomToBottom();
@ -1712,252 +1715,264 @@ function _Chat() {
setAutoScroll(false); setAutoScroll(false);
}} }}
> >
{messages.map((message, i) => { {messages
const isUser = message.role === "user"; // TODO
const isContext = i < context.length; // .filter((m) => !m.isMcpResponse)
const showActions = .map((message, i) => {
i > 0 && const isUser = message.role === "user";
!(message.preview || message.content.length === 0) && const isContext = i < context.length;
!isContext; const showActions =
const showTyping = message.preview || message.streaming; i > 0 &&
!(message.preview || message.content.length === 0) &&
!isContext;
const showTyping = message.preview || message.streaming;
const shouldShowClearContextDivider = const shouldShowClearContextDivider =
i === clearContextIndex - 1; i === clearContextIndex - 1;
return ( return (
<Fragment key={message.id}> <Fragment key={message.id}>
<div <div
className={ className={
isUser isUser
? styles["chat-message-user"] ? styles["chat-message-user"]
: styles["chat-message"] : styles["chat-message"]
} }
> >
<div className={styles["chat-message-container"]}> <div className={styles["chat-message-container"]}>
<div className={styles["chat-message-header"]}> <div className={styles["chat-message-header"]}>
<div className={styles["chat-message-avatar"]}> <div className={styles["chat-message-avatar"]}>
<div className={styles["chat-message-edit"]}> <div className={styles["chat-message-edit"]}>
<IconButton <IconButton
icon={<EditIcon />} icon={<EditIcon />}
aria={Locale.Chat.Actions.Edit} aria={Locale.Chat.Actions.Edit}
onClick={async () => { onClick={async () => {
const newMessage = await showPrompt( const newMessage = await showPrompt(
Locale.Chat.Actions.Edit, Locale.Chat.Actions.Edit,
getMessageTextContent(message), getMessageTextContent(message),
10, 10,
); );
let newContent: string | MultimodalContent[] = let newContent:
newMessage; | string
const images = getMessageImages(message); | MultimodalContent[] = newMessage;
if (images.length > 0) { const images = getMessageImages(message);
newContent = [ if (images.length > 0) {
{ type: "text", text: newMessage }, newContent = [
]; { type: "text", text: newMessage },
for (let i = 0; i < images.length; i++) { ];
newContent.push({ for (let i = 0; i < images.length; i++) {
type: "image_url", newContent.push({
image_url: { type: "image_url",
url: images[i], image_url: {
}, url: images[i],
}); },
} });
}
chatStore.updateTargetSession(
session,
(session) => {
const m = session.mask.context
.concat(session.messages)
.find((m) => m.id === message.id);
if (m) {
m.content = newContent;
} }
},
);
}}
></IconButton>
</div>
{isUser ? (
<Avatar avatar={config.avatar} />
) : (
<>
{["system"].includes(message.role) ? (
<Avatar avatar="2699-fe0f" />
) : (
<MaskAvatar
avatar={session.mask.avatar}
model={
message.model ||
session.mask.modelConfig.model
} }
/> chatStore.updateTargetSession(
)} session,
</> (session) => {
const m = session.mask.context
.concat(session.messages)
.find((m) => m.id === message.id);
if (m) {
m.content = newContent;
}
},
);
}}
></IconButton>
</div>
{isUser ? (
<Avatar avatar={config.avatar} />
) : (
<>
{["system"].includes(message.role) ? (
<Avatar avatar="2699-fe0f" />
) : (
<MaskAvatar
avatar={session.mask.avatar}
model={
message.model ||
session.mask.modelConfig.model
}
/>
)}
</>
)}
</div>
{!isUser && (
<div className={styles["chat-model-name"]}>
{message.model}
</div>
)} )}
</div>
{!isUser && (
<div className={styles["chat-model-name"]}>
{message.model}
</div>
)}
{showActions && ( {showActions && (
<div className={styles["chat-message-actions"]}> <div className={styles["chat-message-actions"]}>
<div className={styles["chat-input-actions"]}> <div className={styles["chat-input-actions"]}>
{message.streaming ? ( {message.streaming ? (
<ChatAction
text={Locale.Chat.Actions.Stop}
icon={<StopIcon />}
onClick={() => onUserStop(message.id ?? i)}
/>
) : (
<>
<ChatAction <ChatAction
text={Locale.Chat.Actions.Retry} text={Locale.Chat.Actions.Stop}
icon={<ResetIcon />} icon={<StopIcon />}
onClick={() => onResend(message)}
/>
<ChatAction
text={Locale.Chat.Actions.Delete}
icon={<DeleteIcon />}
onClick={() => onDelete(message.id ?? i)}
/>
<ChatAction
text={Locale.Chat.Actions.Pin}
icon={<PinIcon />}
onClick={() => onPinMessage(message)}
/>
<ChatAction
text={Locale.Chat.Actions.Copy}
icon={<CopyIcon />}
onClick={() => onClick={() =>
copyToClipboard( onUserStop(message.id ?? i)
getMessageTextContent(message),
)
} }
/> />
{config.ttsConfig.enable && ( ) : (
<>
<ChatAction <ChatAction
text={ text={Locale.Chat.Actions.Retry}
speechStatus icon={<ResetIcon />}
? Locale.Chat.Actions.StopSpeech onClick={() => onResend(message)}
: Locale.Chat.Actions.Speech />
}
icon={ <ChatAction
speechStatus ? ( text={Locale.Chat.Actions.Delete}
<SpeakStopIcon /> icon={<DeleteIcon />}
) : (
<SpeakIcon />
)
}
onClick={() => onClick={() =>
openaiSpeech( onDelete(message.id ?? i)
}
/>
<ChatAction
text={Locale.Chat.Actions.Pin}
icon={<PinIcon />}
onClick={() => onPinMessage(message)}
/>
<ChatAction
text={Locale.Chat.Actions.Copy}
icon={<CopyIcon />}
onClick={() =>
copyToClipboard(
getMessageTextContent(message), getMessageTextContent(message),
) )
} }
/> />
)} {config.ttsConfig.enable && (
</> <ChatAction
)} text={
speechStatus
? Locale.Chat.Actions.StopSpeech
: Locale.Chat.Actions.Speech
}
icon={
speechStatus ? (
<SpeakStopIcon />
) : (
<SpeakIcon />
)
}
onClick={() =>
openaiSpeech(
getMessageTextContent(message),
)
}
/>
)}
</>
)}
</div>
</div> </div>
)}
</div>
{message?.tools?.length == 0 && showTyping && (
<div className={styles["chat-message-status"]}>
{Locale.Chat.Typing}
</div> </div>
)} )}
</div> {/*@ts-ignore*/}
{message?.tools?.length == 0 && showTyping && ( {message?.tools?.length > 0 && (
<div className={styles["chat-message-status"]}> <div className={styles["chat-message-tools"]}>
{Locale.Chat.Typing} {message?.tools?.map((tool) => (
</div> <div
)} key={tool.id}
{/*@ts-ignore*/} title={tool?.errorMsg}
{message?.tools?.length > 0 && ( className={styles["chat-message-tool"]}
<div className={styles["chat-message-tools"]}> >
{message?.tools?.map((tool) => ( {tool.isError === false ? (
<div <ConfirmIcon />
key={tool.id} ) : tool.isError === true ? (
title={tool?.errorMsg} <CloseIcon />
className={styles["chat-message-tool"]} ) : (
> <LoadingButtonIcon />
{tool.isError === false ? ( )}
<ConfirmIcon /> <span>{tool?.function?.name}</span>
) : tool.isError === true ? ( </div>
<CloseIcon /> ))}
) : ( </div>
<LoadingButtonIcon />
)}
<span>{tool?.function?.name}</span>
</div>
))}
</div>
)}
<div className={styles["chat-message-item"]}>
<Markdown
key={message.streaming ? "loading" : "done"}
content={getMessageTextContent(message)}
loading={
(message.preview || message.streaming) &&
message.content.length === 0 &&
!isUser
}
// onContextMenu={(e) => onRightClick(e, message)} // hard to use
onDoubleClickCapture={() => {
if (!isMobileScreen) return;
setUserInput(getMessageTextContent(message));
}}
fontSize={fontSize}
fontFamily={fontFamily}
parentRef={scrollRef}
defaultShow={i >= messages.length - 6}
/>
{getMessageImages(message).length == 1 && (
<img
className={styles["chat-message-item-image"]}
src={getMessageImages(message)[0]}
alt=""
/>
)} )}
{getMessageImages(message).length > 1 && ( <div className={styles["chat-message-item"]}>
<div <Markdown
className={styles["chat-message-item-images"]} key={message.streaming ? "loading" : "done"}
style={ content={getMessageTextContent(message)}
{ loading={
"--image-count": (message.preview || message.streaming) &&
getMessageImages(message).length, message.content.length === 0 &&
} as React.CSSProperties !isUser
} }
> // onContextMenu={(e) => onRightClick(e, message)} // hard to use
{getMessageImages(message).map((image, index) => { onDoubleClickCapture={() => {
return ( if (!isMobileScreen) return;
<img setUserInput(getMessageTextContent(message));
className={ }}
styles["chat-message-item-image-multi"] fontSize={fontSize}
} fontFamily={fontFamily}
key={index} parentRef={scrollRef}
src={image} defaultShow={i >= messages.length - 6}
alt="" />
/> {getMessageImages(message).length == 1 && (
); <img
})} className={styles["chat-message-item-image"]}
src={getMessageImages(message)[0]}
alt=""
/>
)}
{getMessageImages(message).length > 1 && (
<div
className={styles["chat-message-item-images"]}
style={
{
"--image-count":
getMessageImages(message).length,
} as React.CSSProperties
}
>
{getMessageImages(message).map(
(image, index) => {
return (
<img
className={
styles[
"chat-message-item-image-multi"
]
}
key={index}
src={image}
alt=""
/>
);
},
)}
</div>
)}
</div>
{message?.audio_url && (
<div className={styles["chat-message-audio"]}>
<audio src={message.audio_url} controls />
</div> </div>
)} )}
</div>
{message?.audio_url && (
<div className={styles["chat-message-audio"]}>
<audio src={message.audio_url} controls />
</div>
)}
<div className={styles["chat-message-action-date"]}> <div className={styles["chat-message-action-date"]}>
{isContext {isContext
? Locale.Chat.IsContext ? Locale.Chat.IsContext
: message.date.toLocaleString()} : message.date.toLocaleString()}
</div>
</div> </div>
</div> </div>
</div> {shouldShowClearContextDivider && <ClearContextDivider />}
{shouldShowClearContextDivider && <ClearContextDivider />} </Fragment>
</Fragment> );
); })}
})}
</div> </div>
<div className={styles["chat-input-panel"]}> <div className={styles["chat-input-panel"]}>
<PromptHints <PromptHints

View File

@ -253,6 +253,112 @@ Latex inline: \\(x^2\\)
Latex block: $$e=mc^2$$ Latex block: $$e=mc^2$$
`; `;
export const MCP_PRIMITIVES_TEMPLATE = `
[clientId]
{{ clientId }}
[primitives]
{{ primitives }}
`;
// String and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions.
// Here are the functions available in JSONSchema format:
export const MCP_SYSTEM_TEMPLATE = `
You are an AI assistant with access to system tools. Your role is to help users by combining natural language understanding with tool operations when needed.
1. TOOLS AVAILABLE:
{{ MCP_PRIMITIVES }}
2. WHEN TO USE TOOLS:
- When users ask any questions that can be answered by available tools, you should use the tools to answer the user's question.
3. HOW TO USE TOOLS:
A. Tool Call Format:
- Use markdown code blocks with format: \`\`\`json:mcp:{clientId}\`\`\`
- Always include:
* method: "tools/call"
* params:
- name: must match an available primitive name
- arguments: required parameters for the primitive
B. Response Format:
- Tool responses will come as user messages
- Format: \`\`\`json:mcp-response:{clientId}\`\`\`
- Wait for response before making another tool call
C. Important Rules:
- Only ONE tool call per message
- Always use the exact primitive name from available tools
- Include the correct clientId in code block language tag
- Verify arguments match the primitive's requirements
4. INTERACTION FLOW:
A. Understand user's request
B. If tools are needed:
- Explain what you plan to do
- Make the appropriate tool call
- Wait for the response
- Explain the results in user-friendly terms
C. If tools fail:
- Explain the error clearly
- Suggest alternatives or ask for clarification
5. EXAMPLE INTERACTION:
User: "What files do I have on my desktop?"
Assistant: "I'll first check which directories I have access to.
\`\`\`json:mcp:filesystem
{
"method": "tools/call",
"params": {
"name": "list_allowed_directories",
"arguments": {}
}
}
\`\`\`"
User: "\`\`\`json:mcp-response:filesystem
{
"directories": ["/path/to/desktop"]
}
\`\`\`"
Assistant: "I can see that I have access to your desktop directory. Let me list its contents for you.
\`\`\`json:mcp:filesystem
{
"method": "tools/call",
"params": {
"name": "list_directory",
"arguments": {
"path": "/path/to/desktop"
}
}
}
\`\`\`"
User: "\`\`\`json:mcp-response:filesystem
{
"content": [
{
"type": "text",
"text": "[FILE] document.txt\n[DIR] folder1\n[DIR] folder2\n[FILE] image.png\n[FILE] notes.md"
}
]
}
\`\`\`"
Assistant: "I've found the contents of your desktop. Here's what you have:
Files:
- document.txt
- image.png
- notes.md
Directories:
- folder1
- folder2
Would you like to explore any of these directories or perform other operations with these files?"
`;
export const SUMMARIZE_MODEL = "gpt-4o-mini"; export const SUMMARIZE_MODEL = "gpt-4o-mini";
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro"; export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";

View File

@ -1,6 +1,11 @@
"use server"; "use server";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createClient, executeRequest } from "./client"; import {
createClient,
executeRequest,
listPrimitives,
Primitive,
} from "./client";
import { MCPClientLogger } from "./logger"; import { MCPClientLogger } from "./logger";
import conf from "./mcp_config.json"; import conf from "./mcp_config.json";
import { McpRequestMessage } from "./types"; import { McpRequestMessage } from "./types";
@ -8,7 +13,10 @@ import { McpRequestMessage } from "./types";
const logger = new MCPClientLogger("MCP Actions"); const logger = new MCPClientLogger("MCP Actions");
// Use Map to store all clients // Use Map to store all clients
const clientsMap = new Map<string, any>(); const clientsMap = new Map<
string,
{ client: Client; primitives: Primitive[] }
>();
// Whether initialized // Whether initialized
let initialized = false; let initialized = false;
@ -30,8 +38,11 @@ export async function initializeMcpClients() {
try { try {
logger.info(`Initializing MCP client: ${clientId}`); logger.info(`Initializing MCP client: ${clientId}`);
const client = await createClient(config, clientId); const client = await createClient(config, clientId);
clientsMap.set(clientId, client); const primitives = await listPrimitives(client);
logger.success(`Client ${clientId} initialized`); clientsMap.set(clientId, { client, primitives });
logger.success(
`Client [${clientId}] initialized, ${primitives.length} primitives supported`,
);
} catch (error) { } catch (error) {
errorClients.push(clientId); errorClients.push(clientId);
logger.error(`Failed to initialize client ${clientId}: ${error}`); logger.error(`Failed to initialize client ${clientId}: ${error}`);
@ -58,7 +69,7 @@ export async function executeMcpAction(
) { ) {
try { try {
// Find the corresponding client // Find the corresponding client
const client = clientsMap.get(clientId); const client = clientsMap.get(clientId)?.client;
if (!client) { if (!client) {
logger.error(`Client ${clientId} not found`); logger.error(`Client ${clientId} not found`);
return; return;
@ -80,3 +91,16 @@ export async function getAvailableClients() {
(clientId) => !errorClients.includes(clientId), (clientId) => !errorClients.includes(clientId),
); );
} }
// Get all primitives from all clients
export async function getAllPrimitives(): Promise<
{
clientId: string;
primitives: Primitive[];
}[]
> {
return Array.from(clientsMap.entries()).map(([clientId, { primitives }]) => ({
clientId,
primitives,
}));
}

View File

@ -40,13 +40,13 @@ export async function createClient(
return client; return client;
} }
interface Primitive { export interface Primitive {
type: "resource" | "tool" | "prompt"; type: "resource" | "tool" | "prompt";
value: any; value: any;
} }
/** List all resources, tools, and prompts */ /** List all resources, tools, and prompts */
export async function listPrimitives(client: Client) { export async function listPrimitives(client: Client): Promise<Primitive[]> {
const capabilities = client.getServerCapabilities(); const capabilities = client.getServerCapabilities();
const primitives: Primitive[] = []; const primitives: Primitive[] = [];
const promises = []; const promises = [];

View File

@ -4,25 +4,25 @@ import conf from "./mcp_config.json";
const logger = new MCPClientLogger("MCP Server Example", true); const logger = new MCPClientLogger("MCP Server Example", true);
async function main() { const TEST_SERVER = "everything";
logger.info("Connecting to server...");
const client = await createClient(conf.mcpServers.everything, "everything"); async function main() {
logger.info(`All MCP servers: ${Object.keys(conf.mcpServers).join(", ")}`);
logger.info(`Connecting to server ${TEST_SERVER}...`);
const client = await createClient(conf.mcpServers[TEST_SERVER], TEST_SERVER);
const primitives = await listPrimitives(client); const primitives = await listPrimitives(client);
logger.success(`Connected to server everything`); logger.success(`Connected to server ${TEST_SERVER}`);
logger.info( logger.info(
`server capabilities: ${Object.keys( `${TEST_SERVER} supported primitives:\n${JSON.stringify(
client.getServerCapabilities() ?? [], primitives.filter((i) => i.type === "tool"),
).join(", ")}`, null,
2,
)}`,
); );
logger.info("Server supports the following primitives:");
primitives.forEach((primitive) => {
logger.info("\n" + JSON.stringify(primitive, null, 2));
});
} }
main().catch((error) => { main().catch((error) => {

View File

@ -21,6 +21,8 @@ import {
DEFAULT_SYSTEM_TEMPLATE, DEFAULT_SYSTEM_TEMPLATE,
GEMINI_SUMMARIZE_MODEL, GEMINI_SUMMARIZE_MODEL,
KnowledgeCutOffDate, KnowledgeCutOffDate,
MCP_PRIMITIVES_TEMPLATE,
MCP_SYSTEM_TEMPLATE,
ServiceProvider, ServiceProvider,
StoreKey, StoreKey,
SUMMARIZE_MODEL, SUMMARIZE_MODEL,
@ -33,7 +35,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
import { useAccessStore } from "./access"; import { useAccessStore } from "./access";
import { collectModelsWithDefaultModel } from "../utils/model"; import { collectModelsWithDefaultModel } from "../utils/model";
import { createEmptyMask, Mask } from "./mask"; import { createEmptyMask, Mask } from "./mask";
import { executeMcpAction } from "../mcp/actions"; import { executeMcpAction, getAllPrimitives } from "../mcp/actions";
import { extractMcpJson, isMcpJson } from "../mcp/utils"; import { extractMcpJson, isMcpJson } from "../mcp/utils";
const localStorage = safeLocalStorage(); const localStorage = safeLocalStorage();
@ -196,6 +198,24 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
return output; return output;
} }
async function getMcpSystemPrompt(): Promise<string> {
let primitives = await getAllPrimitives();
primitives = primitives.filter((i) =>
i.primitives.some((p) => p.type === "tool"),
);
let primitivesString = "";
primitives.forEach((i) => {
primitivesString += MCP_PRIMITIVES_TEMPLATE.replace(
"{{ clientId }}",
i.clientId,
).replace(
"{{ primitives }}",
i.primitives.map((p) => JSON.stringify(p)).join("\n"),
);
});
return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_PRIMITIVES }}", primitivesString);
}
const DEFAULT_CHAT_STATE = { const DEFAULT_CHAT_STATE = {
sessions: [createEmptySession()], sessions: [createEmptySession()],
currentSessionIndex: 0, currentSessionIndex: 0,
@ -409,7 +429,7 @@ export const useChatStore = createPersistStore(
}); });
// get recent messages // get recent messages
const recentMessages = get().getMessagesWithMemory(); const recentMessages = await get().getMessagesWithMemory();
const sendMessages = recentMessages.concat(userMessage); const sendMessages = recentMessages.concat(userMessage);
const messageIndex = session.messages.length + 1; const messageIndex = session.messages.length + 1;
@ -508,7 +528,7 @@ export const useChatStore = createPersistStore(
} }
}, },
getMessagesWithMemory() { async getMessagesWithMemory() {
const session = get().currentSession(); const session = get().currentSession();
const modelConfig = session.mask.modelConfig; const modelConfig = session.mask.modelConfig;
const clearContextIndex = session.clearContextIndex ?? 0; const clearContextIndex = session.clearContextIndex ?? 0;
@ -524,18 +544,26 @@ export const useChatStore = createPersistStore(
(session.mask.modelConfig.model.startsWith("gpt-") || (session.mask.modelConfig.model.startsWith("gpt-") ||
session.mask.modelConfig.model.startsWith("chatgpt-")); session.mask.modelConfig.model.startsWith("chatgpt-"));
const mcpSystemPrompt = await getMcpSystemPrompt();
var systemPrompts: ChatMessage[] = []; var systemPrompts: ChatMessage[] = [];
systemPrompts = shouldInjectSystemPrompts systemPrompts = shouldInjectSystemPrompts
? [ ? [
createMessage({ createMessage({
role: "system", role: "system",
content: fillTemplateWith("", { content:
...modelConfig, fillTemplateWith("", {
template: DEFAULT_SYSTEM_TEMPLATE, ...modelConfig,
}), template: DEFAULT_SYSTEM_TEMPLATE,
}) + mcpSystemPrompt,
}), }),
] ]
: []; : [
createMessage({
role: "system",
content: mcpSystemPrompt,
}),
];
if (shouldInjectSystemPrompts) { if (shouldInjectSystemPrompts) {
console.log( console.log(
"[Global System Prompt] ", "[Global System Prompt] ",
@ -796,12 +824,12 @@ export const useChatStore = createPersistStore(
? JSON.stringify(result) ? JSON.stringify(result)
: String(result); : String(result);
get().onUserInput( get().onUserInput(
`\`\`\`json:mcp:${mcpRequest.clientId}\n${mcpResponse}\n\`\`\``, `\`\`\`json:mcp-response:${mcpRequest.clientId}\n${mcpResponse}\n\`\`\``,
[], [],
true, true,
); );
}) })
.catch((error) => showToast(String(error))); .catch((error) => showToast("MCP execution failed", error));
} }
} catch (error) { } catch (error) {
console.error("[MCP Error]", error); console.error("[MCP Error]", error);