import { PortfolioApiClient } from './api-client.js';
import { Position } from './types.js';
// Tool interface based on MCP specification
interface Tool {
name: string;
description: string;
inputSchema: {
type: 'object';
properties?: Record<string, any>;
required?: string[];
};
}
export class PortfolioTools {
private apiClient: PortfolioApiClient;
constructor(apiClient: PortfolioApiClient) {
this.apiClient = apiClient;
}
// Define all available tools
getTools(): Tool[] {
return [
{
name: 'get_portfolio_positions',
description: 'Get all current portfolio positions with basic information like ticker, quantity, cost, and account details',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'get_portfolio_pnl',
description: 'Get portfolio profit and loss analysis with current market values, including summary totals and individual position performance',
inputSchema: {
type: 'object',
properties: {
refresh: {
type: 'boolean',
description: 'Whether to force refresh current prices from Yahoo Finance (slower but more accurate)',
default: false
}
},
required: []
}
},
{
name: 'refresh_portfolio_data',
description: 'Force refresh all portfolio price data from Yahoo Finance, including both current and historical prices',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'get_position_details',
description: 'Get detailed information for specific portfolio positions by ticker symbols',
inputSchema: {
type: 'object',
properties: {
tickers: {
type: 'array',
items: {
type: 'string'
},
description: 'Array of ticker symbols to get details for (e.g., ["AAPL", "NVDA", "7940.T"])',
minItems: 1
}
},
required: ['tickers']
}
}
];
}
// Handle tool calls
async handleToolCall(name: string, args: any): Promise<any> {
try {
switch (name) {
case 'get_portfolio_positions':
return await this.getPortfolioPositions();
case 'get_portfolio_pnl':
return await this.getPortfolioPnL(args.refresh || false);
case 'refresh_portfolio_data':
return await this.refreshPortfolioData();
case 'get_position_details':
return await this.getPositionDetails(args.tickers);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
console.error(`Error handling tool call ${name}:`, error);
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`
}],
isError: true
};
}
}
private async getPortfolioPositions() {
const response = await this.apiClient.getPositions();
const summary = `Portfolio contains ${response.count} positions across multiple accounts:\n\n` +
response.positions.map(pos =>
`• ${pos.ticker} (${pos.fullName}): ${pos.quantity.toLocaleString()} shares @ ¥${pos.costPerUnit.toLocaleString()} in ${pos.account}`
).join('\n');
return {
content: [{
type: "text",
text: summary
}],
metadata: {
positions: response.positions,
count: response.count
}
};
}
private async getPortfolioPnL(refresh: boolean = false) {
const response = await this.apiClient.getPnL(refresh);
const summary = response.summary;
const refreshStatus = refresh ? ' (with fresh market data)' : ' (using cached prices)';
const totalPnLFormatted = summary.totalPnlJPY >= 0
? `+¥${summary.totalPnlJPY.toLocaleString()}`
: `-¥${Math.abs(summary.totalPnlJPY).toLocaleString()}`;
const percentageFormatted = summary.totalPnlPercentage >= 0
? `+${summary.totalPnlPercentage.toFixed(2)}%`
: `${summary.totalPnlPercentage.toFixed(2)}%`;
let text = `Portfolio Performance${refreshStatus}:\n\n`;
text += `💰 Total Value: ¥${summary.totalValueJPY.toLocaleString()}\n`;
text += `💸 Total Cost: ¥${summary.totalCostJPY.toLocaleString()}\n`;
text += `📈 Total P&L: ${totalPnLFormatted} (${percentageFormatted})\n\n`;
text += `Top Performers:\n`;
const topPerformers = response.positions
.filter(pos => pos.currentPrice !== null)
.sort((a, b) => b.pnlPercentage - a.pnlPercentage)
.slice(0, 3);
topPerformers.forEach(pos => {
const pnlFormatted = pos.pnlJPY >= 0
? `+¥${pos.pnlJPY.toLocaleString()}`
: `-¥${Math.abs(pos.pnlJPY).toLocaleString()}`;
const percentFormatted = pos.pnlPercentage >= 0
? `+${pos.pnlPercentage.toFixed(2)}%`
: `${pos.pnlPercentage.toFixed(2)}%`;
text += `• ${pos.ticker}: ${pnlFormatted} (${percentFormatted})\n`;
});
return {
content: [{
type: "text",
text: text
}],
metadata: {
summary: response.summary,
positions: response.positions,
timestamp: response.timestamp,
refreshed: refresh
}
};
}
private async refreshPortfolioData() {
await this.apiClient.refreshHistoricalData();
// Also get fresh P&L to show updated data
const pnlResponse = await this.apiClient.getPnL(true);
const text = `✅ Portfolio data refreshed successfully!\n\n` +
`Updated Performance:\n` +
`💰 Total Value: ¥${pnlResponse.summary.totalValueJPY.toLocaleString()}\n` +
`📈 Total P&L: ¥${pnlResponse.summary.totalPnlJPY.toLocaleString()} (${pnlResponse.summary.totalPnlPercentage.toFixed(2)}%)\n` +
`🕐 Last updated: ${new Date(pnlResponse.timestamp).toLocaleString()}`;
return {
content: [{
type: "text",
text: text
}],
metadata: {
refreshed: true,
timestamp: pnlResponse.timestamp,
summary: pnlResponse.summary
}
};
}
private async getPositionDetails(tickers: string[]) {
const pnlResponse = await this.apiClient.getPnL();
const requestedPositions = pnlResponse.positions.filter(pos =>
tickers.some(ticker => ticker.toLowerCase() === pos.ticker.toLowerCase())
);
if (requestedPositions.length === 0) {
return {
content: [{
type: "text",
text: `No positions found for tickers: ${tickers.join(', ')}\n\nAvailable tickers: ${pnlResponse.positions.map(p => p.ticker).join(', ')}`
}]
};
}
let text = `Position Details for ${requestedPositions.length} ticker(s):\n\n`;
requestedPositions.forEach(pos => {
const pnlFormatted = pos.pnlJPY >= 0
? `+¥${pos.pnlJPY.toLocaleString()}`
: `-¥${Math.abs(pos.pnlJPY).toLocaleString()}`;
const percentFormatted = pos.pnlPercentage >= 0
? `+${pos.pnlPercentage.toFixed(2)}%`
: `${pos.pnlPercentage.toFixed(2)}%`;
text += `📊 ${pos.ticker} - ${pos.fullName}\n`;
text += ` Account: ${pos.account}\n`;
text += ` Quantity: ${pos.quantity.toLocaleString()} shares\n`;
text += ` Cost per unit: ¥${pos.costPerUnit.toLocaleString()}\n`;
text += ` Current price: ¥${pos.currentPrice?.toLocaleString() || 'N/A'}\n`;
text += ` Total cost: ¥${pos.costInJPY.toLocaleString()}\n`;
text += ` Current value: ¥${pos.currentValueJPY.toLocaleString()}\n`;
text += ` P&L: ${pnlFormatted} (${percentFormatted})\n\n`;
});
return {
content: [{
type: "text",
text: text
}],
metadata: {
positions: requestedPositions,
requestedTickers: tickers
}
};
}
}