@xmartlabs/vytallink-mcp-server
Version:
A Model Context Protocol (MCP) server that provides access to vytalLink health and fitness data. vytalLink is a comprehensive health platform that aggregates data from wearable devices, fitness apps, and health monitoring systems, providing unified access
330 lines (283 loc) • 9.11 kB
JavaScript
/**
* VytalLink MCP Server Proxy
*
* This server acts as a proxy between MCP clients (like Claude Desktop) and the
* backend API. It implements the MCP protocol while delegating all
* tool definitions and business logic to the backend endpoints.
*
* Architecture:
* - Tool definitions: Fetched from GET /mcp/tools (single source of truth)
* - Tool execution: Forwarded to POST /mcp/call
* - No duplication: All tools defined only in the backend
*/
import fetch from 'node-fetch';
const BASE_URL = process.env.VYTALLINK_BASE_URL || 'https://vytallink.local.xmartlabs.com';
const API_BASE_URL = `${BASE_URL}/mcp/call`;
const TOOLS_URL = `${BASE_URL}/mcp/tools`;
console.error('MCP Server starting...');
console.error(`Backend URL: ${BASE_URL}`);
process.stdin.setEncoding('utf8');
let buffer = '';
let authToken = null;
process.stdin.on('data', (chunk) => {
buffer += chunk;
try {
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep incomplete line in buffer
for (const line of lines) {
if (line.trim()) {
const request = JSON.parse(line);
handleRequest(request);
}
}
} catch (error) {
// Continue reading for incomplete JSON
}
});
process.stdin.on('end', () => {
console.error('MCP Server shutting down...');
process.exit(0);
});
async function handleRequest(request) {
console.error(`Processing request: ${request.method}`);
try {
if (request.method === "initialize") {
await handleInitialize(request);
} else if (request.method === "notifications/initialized") {
// This is a notification, no response needed
console.error('Received initialized notification');
} else if (request.method === "tools/list") {
await handleToolsList(request);
} else if (request.method === "tools/call") {
await handleToolsCall(request);
} else if (request.method === "prompts/list") {
await handlePromptsList(request);
} else if (request.method === "resources/list") {
await handleResourcesList(request);
} else {
console.error(`Unknown method: ${request.method}`);
// Only send error response if request has an ID (not a notification)
if (request.id !== undefined) {
const errorResponse = {
jsonrpc: "2.0",
id: request.id,
error: {
code: -32601,
message: "Method not found",
data: `Unknown method: ${request.method}`
}
};
console.log(JSON.stringify(errorResponse));
}
}
} catch (error) {
console.error('Error processing request:', error);
// Only send error response if request has an ID (not a notification)
if (request.id !== undefined) {
console.log(JSON.stringify({
jsonrpc: "2.0",
id: request.id,
error: {
code: -32603,
message: "Internal error",
data: error.message,
},
}));
}
}
}
async function handleInitialize(request) {
try {
const requestBody = {
method: "initialize",
params: request.params || {}
};
const response = await fetch(API_BASE_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
const result = await response.json();
const initResponse = {
jsonrpc: "2.0",
id: request.id,
result: result,
};
console.log(JSON.stringify(initResponse));
} catch (fetchError) {
console.error('Error calling initialize endpoint:', fetchError);
const errorResponse = {
jsonrpc: "2.0",
id: request.id,
error: {
code: -32603,
message: "Backend server unavailable",
data: fetchError.message
}
};
console.log(JSON.stringify(errorResponse));
}
}
async function handleToolsList(request) {
try {
const toolsResponse = await fetch(TOOLS_URL);
const toolsData = await toolsResponse.json();
const response = {
jsonrpc: "2.0",
id: request.id,
result: {
tools: toolsData.tools
},
};
console.log(JSON.stringify(response));
} catch (fetchError) {
console.error('Error fetching tools from backend endpoint:', fetchError);
const errorResponse = {
jsonrpc: "2.0",
id: request.id,
error: {
code: -32603,
message: "Failed to fetch tools from server",
data: fetchError.message
}
};
console.log(JSON.stringify(errorResponse));
}
}
async function extractAuthToken(text) {
const tokenMatch = text.match(/Access Token: ([a-zA-Z0-9_-]+)/);
if (tokenMatch) {
authToken = tokenMatch[1];
console.error(`Auth token extracted and stored: ${authToken.substring(0, 16)}...`);
return tokenMatch[1];
}
return null;
}
async function callBackendTool(name, args, headers) {
const requestBody = {
method: "call_tool",
params: {
name: name,
arguments: args,
},
};
const response = await fetch(API_BASE_URL, {
method: "POST",
headers: headers,
body: JSON.stringify(requestBody),
});
return await response.json();
}
async function handleOAuthLoginFlow(request, result) {
const text = result.content[0].text;
const authCodeMatch = text.match(/OAuth Code: ([a-zA-Z0-9_-]+)/);
if (!authCodeMatch) {
return null; // No auth code found, return original result
}
const authCode = authCodeMatch[1];
console.error(`OAuth login successful, automatically authorizing with code: ${authCode.substring(0, 16)}...`);
try {
const headers = {
"Content-Type": "application/json",
};
// Call oauth_authorize automatically
const authorizeResult = await callBackendTool("oauth_authorize", {
code: authCode,
state: "random_state"
}, headers);
// Extract and store the auth token
if (authorizeResult.content && authorizeResult.content[0] && authorizeResult.content[0].text) {
await extractAuthToken(authorizeResult.content[0].text);
}
// Return the authorize result
return {
jsonrpc: "2.0",
id: request.id,
result: authorizeResult,
};
} catch (authorizeError) {
console.error('Error during automatic oauth_authorize:', authorizeError);
return null; // Fall back to original result
}
}
async function handleToolsCall(request) {
const { name, arguments: args } = request.params;
console.error(`Calling tool: ${name} with args:`, args);
const headers = {
"Content-Type": "application/json",
};
if (authToken) {
headers["Authorization"] = `Bearer ${authToken}`;
console.error(`Using auth token: ${authToken.substring(0, 16)}...`);
} else {
console.error('No auth token available - request will be unauthenticated');
}
try {
// Prefer direct token flow when possible
let effectiveName = name;
if (name === "oauth_login") {
// If the client asked for oauth_login, try the streamlined direct_login instead
if (args && args.word && args.code) {
console.error("Redirecting oauth_login to direct_login for streamlined auth");
effectiveName = "direct_login";
}
}
const result = await callBackendTool(effectiveName, args, headers);
// Handle oauth_login success - automatically call oauth_authorize
if (name === "oauth_login" && effectiveName === "oauth_login" && result.content && result.content[0] && result.content[0].text) {
const autoAuthResponse = await handleOAuthLoginFlow(request, result);
if (autoAuthResponse) {
console.log(JSON.stringify(autoAuthResponse));
return;
}
}
// Extract auth token for direct flows
if ((effectiveName === "oauth_authorize" || effectiveName === "direct_login") && result.content && result.content[0] && result.content[0].text) {
await extractAuthToken(result.content[0].text);
}
const mcpResponse = {
jsonrpc: "2.0",
id: request.id,
result: result,
};
console.log(JSON.stringify(mcpResponse));
} catch (fetchError) {
console.error('Error calling tool:', fetchError);
const errorResponse = {
jsonrpc: "2.0",
id: request.id,
error: {
code: -32603,
message: "Failed to call tool",
data: fetchError.message
}
};
console.log(JSON.stringify(errorResponse));
}
}
async function handlePromptsList(request) {
// Return empty prompts list since this server doesn't provide prompts
const response = {
jsonrpc: "2.0",
id: request.id,
result: {
prompts: []
}
};
console.log(JSON.stringify(response));
}
async function handleResourcesList(request) {
// Return empty resources list since this server doesn't provide resources
const response = {
jsonrpc: "2.0",
id: request.id,
result: {
resources: []
}
};
console.log(JSON.stringify(response));
}
console.error('MCP Server ready, waiting for requests...');