markov-exa-mcp-server
Version:
A Model Context Protocol server with Exa for web search, academic paper search, and Twitter/X.com search. Provides real-time web searches with configurable tool selection, allowing users to enable or disable specific search capabilities. Supports customiz
424 lines (420 loc) • 20.4 kB
JavaScript
import { z } from "zod";
import axios from "axios";
import { createRequestLogger } from "../utils/logger.js";
const sparringSessions = new Map();
// Helper function to clean up old sessions (keep for 4 hours)
function cleanupSessions() {
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000;
for (const [key, session] of sparringSessions.entries()) {
if (session.createdAt < fourHoursAgo) {
sparringSessions.delete(key);
}
}
}
const OPENAI_API_BASE = "https://api.openai.com/v1";
const REQUEST_TIMEOUT = 60000; // 1 minute
export function registerOpenAISparringTools(server) {
// Tool to start a sparring session
server.tool("openai_sparring_start", "Start an interactive sparring session with o3 to refine your research methodology. O3 will ask clarifying questions to help develop a comprehensive research plan. Note: This creates a research methodology/plan, not the actual research results", {
query: z.string().describe("Your initial research question or topic"),
constraints: z.string().optional().describe("Any constraints or specific requirements for the research"),
context: z.string().optional().describe("Additional context or background information")
}, async ({ query, constraints, context }) => {
const requestId = `openai_sparring_start-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
const logger = createRequestLogger(requestId, 'openai_sparring_start');
try {
const apiKey = process.env.OPENAI_API_KEY || process.env.openaiApiKey;
if (!apiKey) {
logger.error('No OpenAI API key found');
return {
content: [{
type: "text",
text: "OPENAI_API_KEY environment variable is not set"
}],
isError: true
};
}
cleanupSessions();
// Create session ID
const sessionId = `sparring-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
// Build initial prompt for o3
let initialPrompt = `I'm trying to ${query}.`;
if (constraints) {
initialPrompt += `\n\nConstraints: ${constraints}`;
}
if (context) {
initialPrompt += `\n\nContext: ${context}`;
}
initialPrompt += "\n\nPlease ask me 1-2 most important clarifying questions so we can come up with the best research proposal for this topic.";
logger.log(`Creating sparring session: ${sessionId}`);
logger.log(`Initial prompt: ${initialPrompt}`);
// Call o3 to generate clarifying questions
const response = await axios.post(`${OPENAI_API_BASE}/chat/completions`, {
model: "o3",
messages: [
{
role: "system",
content: "You are a research assistant helping to refine research questions. Ask thoughtful, clarifying questions that will help create a comprehensive and well-scoped research proposal. Focus on understanding the user's specific needs, desired outcomes, and any gaps in the initial request."
},
{
role: "user",
content: initialPrompt
}
],
reasoning_effort: "medium",
max_completion_tokens: 2000
}, {
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
timeout: REQUEST_TIMEOUT,
});
const assistantResponse = response.data.choices[0].message.content;
// Create new session
const session = {
id: sessionId,
initialQuery: query,
messages: [
{
role: "user",
content: initialPrompt,
timestamp: Date.now()
},
{
role: "assistant",
content: assistantResponse,
timestamp: Date.now()
}
],
status: "active",
createdAt: Date.now()
};
sparringSessions.set(sessionId, session);
logger.log(`Session created successfully`);
logger.complete();
return {
content: [{
type: "text",
text: JSON.stringify({
sessionId: sessionId,
status: "active",
message: "Sparring session started. O3-mini has asked clarifying questions.",
questions: assistantResponse,
nextStep: "Use 'openai_sparring_continue' to respond and continue the conversation"
}, null, 2)
}]
};
}
catch (error) {
logger.error(error);
if (axios.isAxiosError(error)) {
const errorMessage = error.response?.data?.error?.message || error.response?.data?.message || error.message;
return {
content: [{
type: "text",
text: `Failed to start sparring session: ${errorMessage}`
}],
isError: true
};
}
return {
content: [{
type: "text",
text: `Failed to start sparring session: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool to continue sparring conversation
server.tool("openai_sparring_continue", "Continue the sparring conversation with o3 by responding to questions or asking for more clarification", {
sessionId: z.string().describe("The sparring session ID"),
response: z.string().describe("Your response to o3's questions or additional information")
}, async ({ sessionId, response }) => {
const requestId = `openai_sparring_continue-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
const logger = createRequestLogger(requestId, 'openai_sparring_continue');
try {
const apiKey = process.env.OPENAI_API_KEY || process.env.openaiApiKey;
if (!apiKey) {
logger.error('No OpenAI API key found');
return {
content: [{
type: "text",
text: "OPENAI_API_KEY environment variable is not set"
}],
isError: true
};
}
const session = sparringSessions.get(sessionId);
if (!session) {
return {
content: [{
type: "text",
text: `Sparring session not found: ${sessionId}`
}],
isError: true
};
}
if (session.status === "finalized") {
return {
content: [{
type: "text",
text: "This sparring session has already been finalized. Start a new session or use the existing research proposal."
}],
isError: true
};
}
// Add user response to session
session.messages.push({
role: "user",
content: response,
timestamp: Date.now()
});
// Build messages for o3
const messages = [
{
role: "system",
content: "You are a research assistant helping to refine research questions. Continue asking thoughtful clarifying questions or provide suggestions based on the user's responses. When you feel you have enough information, let the user know they can finalize the session."
}
];
// Add conversation history
session.messages.forEach(msg => {
messages.push({
role: msg.role,
content: msg.content
});
});
logger.log(`Continuing sparring session: ${sessionId}`);
// Call o3 for next response
const apiResponse = await axios.post(`${OPENAI_API_BASE}/chat/completions`, {
model: "o3",
messages: messages,
reasoning_effort: "medium",
max_completion_tokens: 2000
}, {
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
timeout: REQUEST_TIMEOUT,
});
const assistantResponse = apiResponse.data.choices[0].message.content;
// Add assistant response to session
session.messages.push({
role: "assistant",
content: assistantResponse,
timestamp: Date.now()
});
logger.log(`Session updated successfully`);
logger.complete();
return {
content: [{
type: "text",
text: JSON.stringify({
sessionId: sessionId,
status: "active",
turnsCompleted: Math.floor(session.messages.length / 2),
response: assistantResponse,
nextStep: "Continue with 'openai_sparring_continue' or finalize with 'openai_sparring_finalize'"
}, null, 2)
}]
};
}
catch (error) {
logger.error(error);
if (axios.isAxiosError(error)) {
const errorMessage = error.response?.data?.error?.message || error.response?.data?.message || error.message;
return {
content: [{
type: "text",
text: `Failed to continue sparring session: ${errorMessage}`
}],
isError: true
};
}
return {
content: [{
type: "text",
text: `Failed to continue sparring session: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool to finalize sparring and generate research proposal
server.tool("openai_sparring_finalize", "Finalize the sparring session and generate a comprehensive research methodology/plan using gpt-4.1. This creates a detailed plan for HOW to conduct research, not the actual research results. After finalization, you can use the insights to craft a better research query for 'openai_deep_research_create'", {
sessionId: z.string().describe("The sparring session ID"),
additionalNotes: z.string().optional().describe("Any final notes or sources to include in the research proposal")
}, async ({ sessionId, additionalNotes }) => {
const requestId = `openai_sparring_finalize-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
const logger = createRequestLogger(requestId, 'openai_sparring_finalize');
try {
const apiKey = process.env.OPENAI_API_KEY || process.env.openaiApiKey;
if (!apiKey) {
logger.error('No OpenAI API key found');
return {
content: [{
type: "text",
text: "OPENAI_API_KEY environment variable is not set"
}],
isError: true
};
}
const session = sparringSessions.get(sessionId);
if (!session) {
return {
content: [{
type: "text",
text: `Sparring session not found: ${sessionId}`
}],
isError: true
};
}
if (session.status === "finalized") {
return {
content: [{
type: "text",
text: JSON.stringify({
sessionId: sessionId,
status: "finalized",
researchProposal: session.researchProposal,
message: "This session is already finalized. Use the research proposal with 'openai_deep_research_create'."
}, null, 2)
}]
};
}
// Build prompt for gpt-4.1 to generate research proposal
let proposalPrompt = `Based on the following sparring conversation about a research topic, create a comprehensive research proposal that will be used for deep research. The proposal should incorporate all the insights, clarifications, and refinements from the conversation.
Initial Query: ${session.initialQuery}
Conversation:
`;
session.messages.forEach((msg, index) => {
proposalPrompt += `\n${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}\n`;
});
if (additionalNotes) {
proposalPrompt += `\nAdditional notes/sources to include: ${additionalNotes}\n`;
}
proposalPrompt += `
Transform this conversation into a detailed research proposal that:
1. Clearly states the refined research objectives
2. Incorporates all clarifications and insights from the sparring session
3. Defines specific areas to investigate
4. Sets clear boundaries and constraints
5. Identifies key questions to answer
6. Specifies desired outcomes and deliverables
MANDATORY: Include these exact instructions in your proposal:
"In your research response, you MUST:
- Include specific figures, trends, statistics, and measurable outcomes for every claim made
- Prioritize reliable, up-to-date sources: peer-reviewed research, authoritative organizations, regulatory agencies, or official company reports
- Include inline citations and return all source metadata for every fact, statistic, or claim
- Avoid generalizations and ensure every statement is backed by verifiable evidence
- When data is unavailable, explicitly state this rather than making assumptions"`;
logger.log(`Generating research proposal for session: ${sessionId}`);
// Call gpt-4.1 to generate proposal
const response = await axios.post(`${OPENAI_API_BASE}/responses`, {
model: "gpt-4.1",
input: proposalPrompt,
instructions: "Create a comprehensive research proposal based on the sparring conversation. Be detailed and specific."
}, {
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
timeout: REQUEST_TIMEOUT,
});
logger.log(`Response data structure: ${JSON.stringify(Object.keys(response.data))}`);
// Handle the responses API structure
let researchProposal = "No proposal text found in response";
// Check if response has the array structure from responses API
if (Array.isArray(response.data) && response.data.length > 0) {
const firstMessage = response.data[0];
if (firstMessage.content && Array.isArray(firstMessage.content)) {
const outputText = firstMessage.content.find((item) => item.type === 'output_text');
if (outputText && outputText.text) {
researchProposal = outputText.text;
}
}
}
else if (response.data.output_text) {
researchProposal = response.data.output_text;
}
else if (response.data.output) {
researchProposal = response.data.output;
}
else if (response.data.text) {
researchProposal = response.data.text;
}
logger.log(`Extracted proposal length: ${researchProposal.length} characters`);
// Update session
session.status = "finalized";
session.researchProposal = researchProposal;
session.finalizedAt = Date.now();
logger.log(`Session finalized successfully`);
logger.complete();
return {
content: [{
type: "text",
text: JSON.stringify({
sessionId: sessionId,
status: "finalized",
researchProposal: researchProposal,
conversationTurns: Math.floor(session.messages.length / 2),
message: "Research methodology/plan generated successfully! This proposal outlines HOW to conduct research. To get actual research results, use 'openai_deep_research_create' with your specific research question (informed by this methodology).",
nextStep: "Use 'openai_deep_research_create' with your actual research question (not the sessionId) to get research results"
}, null, 2)
}]
};
}
catch (error) {
logger.error(error);
if (axios.isAxiosError(error)) {
const errorMessage = error.response?.data?.error?.message || error.response?.data?.message || error.message;
return {
content: [{
type: "text",
text: `Failed to finalize sparring session: ${errorMessage}`
}],
isError: true
};
}
return {
content: [{
type: "text",
text: `Failed to finalize sparring session: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool to list sparring sessions
server.tool("openai_sparring_list", "List all sparring sessions with their status and basic information", {}, async () => {
cleanupSessions();
const sessions = Array.from(sparringSessions.values()).map(session => ({
sessionId: session.id,
status: session.status,
initialQuery: session.initialQuery,
conversationTurns: Math.floor(session.messages.length / 2),
createdAt: new Date(session.createdAt).toISOString(),
finalizedAt: session.finalizedAt ? new Date(session.finalizedAt).toISOString() : null,
hasProposal: !!session.researchProposal
}));
// Sort by creation date, newest first
sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
return {
content: [{
type: "text",
text: JSON.stringify({
totalSessions: sessions.length,
activeSessions: sessions.filter(s => s.status === 'active').length,
finalizedSessions: sessions.filter(s => s.status === 'finalized').length,
sessions: sessions
}, null, 2)
}]
};
});
}
// Export helper to get sparring session for deep research
export function getSparringSession(sessionId) {
return sparringSessions.get(sessionId);
}