Brightsy MCP Server
by mattlevine
- src
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Get command line arguments
const args = process.argv.slice(2);
let agent_id: string | undefined = undefined;
let api_key: string | undefined = undefined;
let initialMessage: string | undefined = undefined;
let tool_name: string = process.env.BRIGHTSY_TOOL_NAME || "brightsy";
// Parse command-line arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
// Handle arguments with equals sign (--key=value)
if (arg.startsWith('--') && arg.includes('=')) {
const [key, value] = arg.substring(2).split('=', 2);
if (key === 'agent-id' || key === 'agent_id') {
agent_id = value;
} else if (key === 'api-key' || key === 'api_key') {
api_key = value;
} else if (key === 'message') {
initialMessage = value;
} else if (key === 'tool-name' || key === 'tool_name') {
tool_name = value;
}
}
// Handle arguments with space (--key value)
else if (arg.startsWith('--')) {
const key = arg.substring(2);
const nextArg = i + 1 < args.length ? args[i + 1] : undefined;
if (nextArg && !nextArg.startsWith('--')) {
if (key === 'agent-id' || key === 'agent_id') {
agent_id = nextArg;
} else if (key === 'api-key' || key === 'api_key') {
api_key = nextArg;
} else if (key === 'message') {
initialMessage = nextArg;
} else if (key === 'tool-name' || key === 'tool_name') {
tool_name = nextArg;
}
i++; // Skip the next argument as we've used it as a value
}
}
// Handle positional arguments
else if (agent_id === undefined) {
agent_id = arg;
} else if (api_key === undefined) {
api_key = arg;
} else if (tool_name === undefined) {
tool_name = arg;
} else if (initialMessage === undefined) {
initialMessage = arg;
}
}
// Check for environment variables if not provided via command line
if (!agent_id) {
agent_id = process.env.BRIGHTSY_AGENT_ID;
}
if (!api_key) {
api_key = process.env.BRIGHTSY_API_KEY;
}
console.error(`Parsed arguments: agent_id=${agent_id}, tool_name=${tool_name}, message=${initialMessage ? 'provided' : 'not provided'}`);
if (!agent_id || !api_key) {
console.error('Usage: node dist/index.js <agent_id> <api_key> [tool_name] [message]');
console.error(' or: node dist/index.js --agent-id=<agent_id> --api-key=<api_key> [--tool-name=<tool_name>] [--message=<message>]');
console.error(' or: node dist/index.js --agent-id <agent_id> --api-key <api_key> [--tool-name <tool_name>] [--message <message>]');
console.error('');
console.error('Environment variables:');
console.error(' BRIGHTSY_AGENT_ID: Agent ID (alternative to command line argument)');
console.error(' BRIGHTSY_API_KEY: API Key (alternative to command line argument)');
console.error(' BRIGHTSY_TOOL_NAME: Tool name (default: brightsy)');
console.error(' BRIGHTSY_AGENT_API_URL: Base URL for agent API (default: https://brightsy.ai)');
process.exit(1);
}
// Create server instance
const server = new McpServer({
name: "brightsy-mcp",
version: "1.0.0",
});
// Add conversation history state to maintain session state
// This will store messages across multiple tool invocations
interface Message {
role: string;
content: string | any[];
}
// Initialize conversation history
let conversationHistory: Message[] = [];
// Get the agent API base URL from environment variable or use default
const agentApiBaseUrl = process.env.BRIGHTSY_AGENT_API_URL || 'https://brightsy.ai';
// Helper function to process content from the agent response
function processContent(content: any): { type: "text"; text: string }[] {
if (!content) {
return [{ type: "text", text: "No content in response" }];
}
// If content is a string, return it as text
if (typeof content === 'string') {
return [{ type: "text", text: content }];
}
// If content is an array, process each item
if (Array.isArray(content)) {
return content.map(item => {
if (typeof item === 'string') {
return { type: "text", text: item };
}
// Handle content blocks (text, image, etc.)
if (item.text) {
return { type: "text", text: item.text };
}
// For other types, convert to string representation
return {
type: "text",
text: `[${item.type} content: ${JSON.stringify(item)}]`
};
});
}
// If we can't process it, return a string representation
return [{ type: "text", text: JSON.stringify(content) }];
}
// Register the agent proxy tool
server.tool(
tool_name,
`Proxy requests to an Brightsy AI agent`,
{
messages: z.array(
z.object({
role: z.string().describe("The role of the message sender"),
content: z.union([z.string(), z.array(z.any())]).describe("The content of the message")
})
).describe("The messages to send to the agent")
},
async ({ messages }) => {
try {
console.error(`Agent proxy tool called with messages:`);
console.error(JSON.stringify(messages, null, 2));
// Check for special command to clear history
if (messages.length === 1 &&
messages[0].role === 'user' &&
typeof messages[0].content === 'string' &&
messages[0].content.trim().toLowerCase() === 'clear history') {
conversationHistory = [];
console.error('Conversation history cleared');
return {
content: [
{
type: "text",
text: "Conversation history has been cleared.",
},
],
};
}
// Check for test commands
if (messages.length === 1 &&
messages[0].role === 'user' &&
typeof messages[0].content === 'string') {
const content = messages[0].content.trim();
// Add the message to conversation history before processing
conversationHistory = [...conversationHistory, messages[0]];
// Handle test:echo command
if (content.startsWith('test:echo ')) {
const message = content.substring('test:echo '.length);
return {
content: [
{
type: "text",
text: "```\n" + message + "\n```"
}
]
};
}
// Handle test:history command
if (content === 'test:history') {
console.error(`Checking history with ${conversationHistory.length} messages:`);
conversationHistory.forEach((msg, i) => {
console.error(`[${i}] ${msg.role}: ${typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)}`);
});
if (conversationHistory.length > 1) {
// Find the first non-history-check message
for (let i = 0; i < conversationHistory.length - 1; i++) {
const msg = conversationHistory[i];
console.error(`Checking message ${i}: ${msg.role} - ${typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)}`);
if (msg.role === 'user' && typeof msg.content === 'string' && !msg.content.includes('test:history')) {
console.error(`Found message: ${msg.content}`);
return {
content: [
{
type: "text",
text: msg.content
}
]
};
}
}
}
console.error('No suitable message found in history');
return {
content: [
{
type: "text",
text: "I notice you want to test something related to history"
}
]
};
}
// Handle test:simulate command
if (content.startsWith('test:simulate ')) {
const message = content.substring('test:simulate '.length);
return {
content: [
{
type: "text",
text: message
}
]
};
}
}
// Add new messages to conversation history (for non-test commands)
conversationHistory = [...conversationHistory, ...messages];
console.error(`Using conversation history with ${conversationHistory.length} messages`);
const agentUrl = `${agentApiBaseUrl}/api/v1beta/agent/${agent_id}/chat/completion`;
console.error(`Forwarding request to agent: ${agent_id}`);
console.error(`Agent URL: ${agentUrl}`);
const requestBody = {
messages: conversationHistory,
stream: false
};
console.error(`Request body: ${JSON.stringify(requestBody, null, 2)}`);
const response = await fetch(agentUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${api_key}`
},
body: JSON.stringify(requestBody)
});
console.error(`Response status: ${response.status} ${response.statusText}`);
console.error(`Response headers: ${JSON.stringify(Object.fromEntries([...response.headers]), null, 2)}`);
// Clone the response to log the raw response body
const responseClone = response.clone();
const rawResponseText = await responseClone.text();
console.error(`Raw response body: ${rawResponseText}`);
if (!response.ok) {
const errorText = await response.text();
console.error(`Error from agent: ${response.status} ${errorText}`);
return {
content: [
{
type: "text",
text: `Error from agent: ${errorText}`,
},
],
};
}
// Try to parse the response as JSON
let data;
try {
data = JSON.parse(rawResponseText);
console.error(`Response received from agent and parsed as JSON`);
} catch (parseError) {
console.error(`Failed to parse response as JSON: ${parseError}`);
return {
content: [
{
type: "text",
text: `Error parsing response: ${parseError}\nRaw response: ${rawResponseText}`,
},
],
};
}
console.error(`Response data: ${JSON.stringify(data, null, 2)}`);
// Extract the assistant's response
const assistantMessage = data.choices?.[0]?.message;
if (!assistantMessage) {
console.error(`No assistant message found in response: ${JSON.stringify(data, null, 2)}`);
return {
content: [
{
type: "text",
text: "No message in agent response",
},
],
};
}
console.error(`Assistant message: ${JSON.stringify(assistantMessage, null, 2)}`);
// Add the assistant's response to the conversation history
if (assistantMessage) {
conversationHistory.push({
role: assistantMessage.role || 'assistant',
content: assistantMessage.content
});
console.error(`Added assistant response to history. History now has ${conversationHistory.length} messages`);
}
// Handle the case where content is already an array of content blocks
if (Array.isArray(assistantMessage.content)) {
console.error(`Content is an array, processing directly`);
// Map the content array to the expected format
const processedContent = assistantMessage.content.map((item: any) => {
if (typeof item === 'string') {
return { type: "text", text: item };
}
// Handle content blocks (text, image, etc.)
if (item.text) {
return { type: "text", text: item.text };
}
// For other types, convert to string representation
return {
type: "text",
text: `[${item.type || 'unknown'} content: ${JSON.stringify(item)}]`
};
});
console.error(`Directly processed content: ${JSON.stringify(processedContent, null, 2)}`);
return {
content: processedContent,
};
}
// Process the content from the assistant's message (for string or other formats)
const processedContent = processContent(assistantMessage.content);
console.error(`Processed content: ${JSON.stringify(processedContent, null, 2)}`);
return {
content: processedContent,
};
} catch (error) {
console.error('Error forwarding request:', error);
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
},
);
// If an initial message was provided, process it immediately after server starts
async function processInitialMessage() {
if (initialMessage) {
console.error(`Processing initial message: ${initialMessage}`);
try {
// Create a messages array with the initial message
const messages = [
{ role: "user", content: initialMessage }
];
// Add to conversation history
conversationHistory.push(...messages);
// Instead of trying to access the tool directly, make a request to the local API
const agentUrl = `${agentApiBaseUrl}/api/v1beta/agent/${agent_id}/chat/completion`;
console.error(`Forwarding initial message to agent: ${agent_id}`);
const requestBody = {
messages: conversationHistory,
stream: false
};
const response = await fetch(agentUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${api_key}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
console.error(`Error from agent: ${response.status} ${errorText}`);
console.log(`Error from agent: ${errorText}`);
return;
}
const data = await response.json();
console.error(`Response received from agent`);
// Extract the assistant's response
const assistantMessage = data.choices?.[0]?.message;
if (!assistantMessage) {
console.log("No message in agent response");
return;
}
// Add the assistant's response to the conversation history
if (assistantMessage) {
conversationHistory.push({
role: assistantMessage.role || 'assistant',
content: assistantMessage.content
});
console.error(`Added assistant response to history. History now has ${conversationHistory.length} messages`);
}
// Handle the case where content is already an array of content blocks
if (Array.isArray(assistantMessage.content)) {
// Map the content array to extract text
const textOutput = assistantMessage.content
.map((item: any) => {
if (typeof item === 'string') return item;
if (item.text) return item.text;
return JSON.stringify(item);
})
.join("\n");
console.log(textOutput);
return;
}
// Process and output the content
const processedContent = processContent(assistantMessage.content);
const textOutput = processedContent
.filter(item => item.type === "text")
.map(item => item.text)
.join("\n");
console.log(textOutput);
} catch (error) {
console.error("Error processing initial message:", error);
console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
// Start the server
async function main() {
try {
const transport = new StdioServerTransport();
// Add event listeners for process events
process.on('SIGINT', () => {
console.error('Received SIGINT signal, shutting down...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.error('Received SIGTERM signal, shutting down...');
process.exit(0);
});
// Keep stdin open
process.stdin.resume();
// Connect to the transport
console.error(`Connecting to transport...`);
await server.connect(transport);
console.error(`Brightsy MCP Server running on stdio`);
console.error(`Connected to agent: ${agent_id}`);
console.error(`Registered tool name: ${tool_name}`);
console.error(`Agent API URL: ${agentApiBaseUrl}`);
console.error(`Ready to receive requests`);
// Process initial message if provided
await processInitialMessage();
} catch (error) {
console.error("Error starting server:", error);
process.exit(1);
}
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});