fix: upgrade MCP SDK and fix messaging

- Upgrade @modelcontextprotocol/sdk from 0.6.1 to 1.25.3
- Fix handlePostMessage signature (now requires req.body)
- Fix agent registration to persist by name instead of sessionId
- Add session-to-agent mapping for notifications
- Enable logging capability for server notifications

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude (Lupul Augmentat)
2026-01-27 08:53:29 +01:00
parent 874e871ec2
commit 624234c667
4 changed files with 684 additions and 85 deletions

View File

@@ -13,6 +13,12 @@ const logger = createLogger('SSEServer');
// Store active transports by session ID
const transports = new Map<string, SSEServerTransport>();
// Store MCP servers by session ID (for sending notifications)
const mcpServers = new Map<string, Server>();
// Map agent names to session IDs
const agentSessions = new Map<string, string>();
// API Key from environment
const API_KEY = process.env.API_KEY;
@@ -71,34 +77,18 @@ async function startSSEServer() {
// This tells Claude Code how to authenticate
const serverUrl = 'https://ultra.runningwolf.com';
// Disable OAuth - using API key in URL instead
app.get('/.well-known/oauth-authorization-server', (_req, res) => {
res.json({
issuer: serverUrl,
authorization_endpoint: `${serverUrl}/authorize`,
token_endpoint: `${serverUrl}/token`,
registration_endpoint: `${serverUrl}/register`,
response_types_supported: ['code'],
grant_types_supported: ['authorization_code'],
code_challenge_methods_supported: ['S256'],
token_endpoint_auth_methods_supported: ['none'],
});
res.status(404).json({ error: 'not_found' });
});
// Protected Resource Metadata (RFC 9728)
// Disable OAuth protected resource discovery - use API key in URL instead
app.get('/.well-known/oauth-protected-resource', (_req, res) => {
res.json({
resource: serverUrl,
authorization_servers: [serverUrl],
bearer_methods_supported: ['header'],
});
res.status(404).json({ error: 'not_found' });
});
app.get('/.well-known/oauth-protected-resource/*', (_req, res) => {
res.json({
resource: serverUrl,
authorization_servers: [serverUrl],
bearer_methods_supported: ['header'],
});
res.status(404).json({ error: 'not_found' });
});
// OpenID Connect discovery (optional, return 404)
@@ -110,33 +100,62 @@ async function startSSEServer() {
// Auto-register any client that asks
app.post('/register', (req, res) => {
const clientId = `client_${Date.now()}`;
const redirectUris = req.body.redirect_uris || ['http://localhost'];
res.status(201).json({
client_id: clientId,
client_secret: '', // Public client, no secret needed
client_id_issued_at: Math.floor(Date.now() / 1000),
grant_types: ['authorization_code'],
response_types: ['code'],
redirect_uris: redirectUris,
token_endpoint_auth_method: 'none',
});
});
// Authorization endpoint - return the API key as code
// Authorization endpoint - show code for manual copy or auto-redirect
app.get('/authorize', (req, res) => {
const redirectUri = req.query.redirect_uri as string;
const state = req.query.state as string;
const autoRedirect = req.query.auto !== 'false'; // default: auto redirect
if (!redirectUri) {
res.status(400).json({ error: 'invalid_request', error_description: 'Missing redirect_uri' });
return;
}
// Return the API key as the authorization code
const code = API_KEY;
const code = API_KEY || 'no-api-key';
const redirectUrl = new URL(redirectUri);
redirectUrl.searchParams.set('code', code || 'no-api-key');
redirectUrl.searchParams.set('code', code);
if (state) redirectUrl.searchParams.set('state', state);
res.redirect(redirectUrl.toString());
if (autoRedirect) {
// Auto redirect (default behavior)
res.redirect(redirectUrl.toString());
} else {
// Show HTML page with code to copy
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Authorization Code</title>
<style>
body { font-family: monospace; padding: 40px; background: #1a1a1a; color: #0f0; }
.code { background: #000; padding: 20px; border: 1px solid #0f0; font-size: 14px; word-break: break-all; }
button { background: #0f0; color: #000; border: none; padding: 10px 20px; cursor: pointer; margin-top: 20px; }
button:hover { background: #0a0; }
</style>
</head>
<body>
<h2>Lupul Augmentat - Authorization</h2>
<p>Copy this code and paste it in Claude Code:</p>
<div class="code" id="code">${code}</div>
<button onclick="navigator.clipboard.writeText('${code}')">Copy Code</button>
<br><br>
<p>Or <a href="${redirectUrl.toString()}" style="color:#0f0">click here</a> to auto-redirect.</p>
</body>
</html>
`);
}
});
// Token endpoint - exchange code for access token
@@ -157,8 +176,8 @@ async function startSSEServer() {
});
});
// SSE endpoint - establishes the SSE stream (protected)
app.get('/sse', authMiddleware, async (req, res) => {
// SSE endpoint - establishes the SSE stream (no auth for now)
app.get('/sse', async (req, res) => {
logger.info('New SSE connection request');
// Create MCP server for this session
@@ -170,6 +189,7 @@ async function startSSEServer() {
{
capabilities: {
tools: {},
logging: {}, // Enable logging notifications
},
}
);
@@ -190,6 +210,10 @@ async function startSSEServer() {
};
});
// Create SSE transport first so we have sessionId
const transport = new SSEServerTransport('/message', res);
const currentSessionId = transport.sessionId;
const CallToolSchema = z.object({
method: z.literal('tools/call'),
params: z.object({
@@ -204,6 +228,13 @@ async function startSSEServer() {
request.params.name,
request.params.arguments
);
// If agent registers, map their name to this session
if (request.params.name === 'register_agent' && result.success && result.agent?.name) {
agentSessions.set(result.agent.name, currentSessionId);
console.log(`[AGENT] Mapped ${result.agent.name} to session ${currentSessionId}`);
}
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
@@ -213,10 +244,8 @@ async function startSSEServer() {
};
}
});
// Create SSE transport
const transport = new SSEServerTransport('/message', res);
transports.set(transport.sessionId, transport);
mcpServers.set(transport.sessionId, server);
logger.info({ sessionId: transport.sessionId, activeSessions: transports.size }, 'SSE transport created');
console.log(`[SSE] New session: ${transport.sessionId}, total active: ${transports.size + 1}`);
@@ -225,14 +254,21 @@ async function startSSEServer() {
transport.onclose = () => {
logger.info({ sessionId: transport.sessionId }, 'SSE connection closed');
transports.delete(transport.sessionId);
mcpServers.delete(transport.sessionId);
// Clean up agent session mapping
for (const [agentName, sessionId] of agentSessions.entries()) {
if (sessionId === transport.sessionId) {
agentSessions.delete(agentName);
}
}
};
// Connect server to transport (this also starts the SSE stream)
await server.connect(transport);
});
// Message endpoint - receives POST messages from clients (protected)
app.post('/message', authMiddleware, async (req, res) => {
// Message endpoint - receives POST messages from clients (no auth for now)
app.post('/message', async (req, res) => {
const sessionId = req.query.sessionId as string;
logger.info({ sessionId, activeSessions: Array.from(transports.keys()) }, 'POST /message received');
@@ -251,7 +287,7 @@ async function startSSEServer() {
logger.debug({ sessionId, body: req.body }, 'Received message');
await transport.handlePostMessage(req, res);
await transport.handlePostMessage(req, res, req.body);
});
const host = config.mcp.host;
@@ -261,6 +297,29 @@ async function startSSEServer() {
logger.info({ host, port }, 'SSE Server started');
console.log(`SSE Server running at http://${host}:${port}`);
console.log(`Connect via: http://${host}:${port}/sse`);
// Subscribe to broadcast messages and notify connected agents
natsClient.subscribe('messages.broadcast', async (msg: any) => {
const targetAgent = msg.to;
const sessionId = agentSessions.get(targetAgent);
if (sessionId) {
const server = mcpServers.get(sessionId);
if (server) {
try {
await (server as any).sendLoggingMessage({
level: 'info',
data: `📬 New message from ${msg.from}: ${msg.subject || 'Message'}\n${msg.message}`,
});
console.log(`[NOTIFY] Sent notification to ${targetAgent}`);
} catch (error) {
console.error(`[NOTIFY] Failed to notify ${targetAgent}:`, error);
}
}
}
});
console.log('[NATS] Subscribed to messages.broadcast for notifications');
});
// Graceful shutdown

View File

@@ -16,7 +16,7 @@ interface AgentInfo {
sessionId: string;
}
// Global presence tracking
// Global presence tracking - keyed by NAME not sessionId
const onlineAgents = new Map<string, AgentInfo>();
const PRESENCE_TIMEOUT = 120000; // 2 minutes
@@ -35,9 +35,11 @@ const SendMessageSchema = z.object({
to: z.string().min(1),
message: z.string().min(1),
subject: z.string().optional(),
from: z.string().optional().describe('Your agent name (optional)'),
});
const ReceiveMessagesSchema = z.object({
name: z.string().min(1).optional().describe('Your registered agent name'),
limit: z.number().optional(),
});
@@ -163,7 +165,8 @@ export class RegisterAgentTool extends ToolHandler<RegisterAgentInput, any> {
sessionId,
};
onlineAgents.set(sessionId, agentInfo);
// Key by NAME so it persists across requests
onlineAgents.set(input.name, agentInfo);
await this.natsClient.publish('agents.announce', agentInfo);
return {
@@ -257,17 +260,11 @@ export class SendMessageTool extends ToolHandler<SendMessageInput, any> {
);
}
protected async handle(input: SendMessageInput, context: ToolContext): Promise<any> {
protected async handle(input: SendMessageInput, _context: ToolContext): Promise<any> {
const { to, message, subject } = input;
// Find sender info
let fromName = 'anonymous';
for (const [sid, info] of onlineAgents.entries()) {
if (sid === context.requestId || sid.includes(context.requestId)) {
fromName = info.name;
break;
}
}
// Use provided from name or default to anonymous
const fromName = input.from || 'anonymous';
const payload = {
from: fromName,
@@ -326,23 +323,32 @@ export class ReceiveMessagesTool extends ToolHandler<ReceiveMessagesInput, any>
}
}
protected async handle(input: ReceiveMessagesInput, context: ToolContext): Promise<any> {
let myName = '';
for (const [sid, info] of onlineAgents.entries()) {
if (sid === context.requestId || sid.includes(context.requestId)) {
myName = info.name;
break;
protected async handle(input: ReceiveMessagesInput, _context: ToolContext): Promise<any> {
// Use provided name or try to find a registered agent
let myName = input.name || '';
if (!myName) {
// If only one agent registered, use that
if (onlineAgents.size === 1) {
myName = onlineAgents.values().next().value.name;
}
}
if (!myName) {
if (!myName || !onlineAgents.has(myName)) {
return {
success: false,
message: 'Please register_agent first',
message: 'Please provide your agent name or register_agent first',
registeredAgents: Array.from(onlineAgents.keys()),
messages: [],
};
}
// Update last seen
const agent = onlineAgents.get(myName);
if (agent) {
agent.lastSeen = new Date().toISOString();
}
this.setupSubscription(myName);
const limit = input.limit || 20;