MCP Portfolio Advisor
This guide builds a custom MCP client from scratch that connects to Suwappu's MCP endpoint, discovers tools dynamically, fetches your portfolio and prices, then uses an AI model (or rule-based fallback) to analyze your holdings and recommend trades. It demonstrates full MCP protocol interaction: handshake, tool discovery, and multi-tool orchestration.
What the Script Does
1. Loads your API key from environment variables
2. Builds an McpClient class that wraps POST /mcp with JSON-RPC 2.0
3. Performs the MCP initialize handshake
4. Discovers available tools via tools/list and prints their schemas
5. Calls get_portfolio to fetch wallet holdings
6. Calls get_prices to fetch current prices with 24h change
7. Calls list_chains to show supported networks
8. Formats data into an AI prompt (or applies rule-based analysis if no AI key is set)
9. Generates an advisory report: concentration risk, momentum signals, diversification score
10. Calls get_quote for any recommended trades to show real costs
Python Version
class=class="hl-str">"hl-comment">#!/usr/bin/env python3
class="hl-str">""class="hl-str">"
Suwappu MCP Portfolio Advisor — Python
Custom MCP client that discovers tools, fetches data, and generates investment advice.
"class="hl-str">""
import os
import sys
import json
import requests
MCP_URL = class="hl-str">"https:class="hl-commentclass="hl-str">">//api.suwappu.bot/mcp"
class McpClient:
class="hl-str">""class="hl-str">"Minimal MCP client wrapping the Suwappu MCP HTTP endpoint."class="hl-str">""
def __init__(self, api_key):
self.api_key = api_key
self.headers = {
class="hl-str">"Authorization": fclass="hl-str">"Bearer {api_key}",
class="hl-str">"Content-Type": class="hl-str">"application/json",
}
self.request_id = 0
self.tools = []
def _next_id(self):
self.request_id += 1
return self.request_id
def _send(self, method, params=None):
class="hl-str">""class="hl-str">"Send a JSON-RPC 2.0 request to the MCP endpoint."class="hl-str">""
payload = {
class="hl-str">"jsonrpc": class="hl-str">"2.0",
class="hl-str">"id": self._next_id(),
class="hl-str">"method": method,
class="hl-str">"params": params or {},
}
response = requests.post(MCP_URL, headers=self.headers, json=payload)
response.raise_for_status()
data = response.json()
if class="hl-str">"error" in data:
raise Exception(fclass="hl-str">"MCP error {data[class="hl-str">'error'][class="hl-str">'code']}: {data[class="hl-str">'error'][class="hl-str">'message']}")
return data[class="hl-str">"result"]
def initialize(self):
class="hl-str">""class="hl-str">"Perform the MCP handshake."class="hl-str">""
result = self._send(class="hl-str">"initialize")
server = result[class="hl-str">"serverInfo"]
print(fclass="hl-str">"Connected to {server[class="hl-str">'name']} v{server[class="hl-str">'version']}")
print(fclass="hl-str">"Protocol: {result[class="hl-str">'protocolVersion']}")
return result
def list_tools(self):
class="hl-str">""class="hl-str">"Discover available tools and their schemas."class="hl-str">""
result = self._send(class="hl-str">"tools/list")
self.tools = result.get(class="hl-str">"tools", [])
return self.tools
def call_tool(self, name, arguments=None):
class="hl-str">""class="hl-str">"Call a tool by name and return parsed result."class="hl-str">""
result = self._send(class="hl-str">"tools/call", {
class="hl-str">"name": name,
class="hl-str">"arguments": arguments or {},
})
class=class="hl-str">"hl-comment"># MCP returns content as array of parts with JSON strings
content = result.get(class="hl-str">"content", [])
for part in content:
if part[class="hl-str">"type"] == class="hl-str">"text":
try:
return json.loads(part[class="hl-str">"text"])
except json.JSONDecodeError:
return part[class="hl-str">"text"]
return content
def analyze_with_ai(portfolio_data, price_data, chains_data):
class="hl-str">""class="hl-str">"Use OpenAI or Anthropic to analyze the portfolio. Falls back to rules."class="hl-str">""
openai_key = os.environ.get(class="hl-str">"OPENAI_API_KEY")
anthropic_key = os.environ.get(class="hl-str">"ANTHROPIC_API_KEY")
prompt = fclass="hl-str">""class="hl-str">"You are a crypto portfolio advisor. Analyze this portfolio and provide actionable recommendations.
Portfolio Holdings:
{json.dumps(portfolio_data, indent=2)}
Current Prices (with 24h change):
{json.dumps(price_data, indent=2)}
Supported Chains:
{json.dumps(chains_data, indent=2)}
Analyze for:
1. Concentration risk (any single token >50% of portfolio?)
2. Momentum signals (tokens with >5% 24h change)
3. Diversification score (how many tokens, how spread across chains)
4. Stablecoin ratio (is there enough stable allocation for risk management?)
Provide 2-3 specific trade recommendations with reasoning. Format as a clear report."class="hl-str">""
if openai_key:
return _call_openai(openai_key, prompt)
elif anthropic_key:
return _call_anthropic(anthropic_key, prompt)
else:
return None
def _call_openai(api_key, prompt):
class="hl-str">""class="hl-str">"Call OpenAI API for analysis."class="hl-str">""
response = requests.post(
class="hl-str">"https:class="hl-commentclass="hl-str">">//api.openai.com/v1/chat/completions",
headers={
class="hl-str">"Authorization": fclass="hl-str">"Bearer {api_key}",
class="hl-str">"Content-Type": class="hl-str">"application/json",
},
json={
class="hl-str">"model": class="hl-str">"gpt-4o",
class="hl-str">"messages": [{class="hl-str">"role": class="hl-str">"user", class="hl-str">"content": prompt}],
class="hl-str">"temperature": 0.3,
},
)
response.raise_for_status()
return response.json()[class="hl-str">"choices"][0][class="hl-str">"message"][class="hl-str">"content"]
def _call_anthropic(api_key, prompt):
class="hl-str">""class="hl-str">"Call Anthropic API for analysis."class="hl-str">""
response = requests.post(
class="hl-str">"https:class="hl-commentclass="hl-str">">//api.anthropic.com/v1/messages",
headers={
class="hl-str">"x-api-key": api_key,
class="hl-str">"anthropic-version": class="hl-str">"2023-06-01",
class="hl-str">"Content-Type": class="hl-str">"application/json",
},
json={
class="hl-str">"model": class="hl-str">"claude-sonnet-4-20250514",
class="hl-str">"max_tokens": 1024,
class="hl-str">"messages": [{class="hl-str">"role": class="hl-str">"user", class="hl-str">"content": prompt}],
},
)
response.raise_for_status()
return response.json()[class="hl-str">"content"][0][class="hl-str">"text"]
def rule_based_analysis(portfolio_data, price_data):
class="hl-str">""class="hl-str">"Simple rule-based analysis when no AI key is available."class="hl-str">""
balances = portfolio_data.get(class="hl-str">"balances", [])
total_usd = portfolio_data.get(class="hl-str">"total_usd", 0)
recommendations = []
if total_usd == 0:
return class="hl-str">"Portfolio is empty. Fund your wallet to get started."
report = []
report.append(class="hl-str">"=" * 55)
report.append(class="hl-str">" PORTFOLIO ADVISORY REPORT")
report.append(class="hl-str">"=" * 55)
class=class="hl-str">"hl-comment"># 1. Concentration risk
report.append(class="hl-str">"\n 1. CONCENTRATION RISK")
for bal in balances:
pct = (bal[class="hl-str">"usd_value"] / total_usd) * 100 if total_usd > 0 else 0
if pct > 50:
report.append(fclass="hl-str">" WARNING: {bal[class="hl-str">'symbol']} is {pct:.1f}% of portfolio (>50%)")
recommendations.append({
class="hl-str">"action": class="hl-str">"sell",
class="hl-str">"token": bal[class="hl-str">"symbol"],
class="hl-str">"reason": fclass="hl-str">"Over-concentrated at {pct:.1f}%",
class="hl-str">"target": class="hl-str">"Reduce to <40% by selling into USDC or diversifying",
})
elif pct > 30:
report.append(fclass="hl-str">" WATCH: {bal[class="hl-str">'symbol']} at {pct:.1f}% — approaching concentration limit")
else:
report.append(fclass="hl-str">" OK: {bal[class="hl-str">'symbol']} at {pct:.1f}%")
class=class="hl-str">"hl-comment"># 2. Momentum signals
report.append(class="hl-str">"\n 2. MOMENTUM SIGNALS (24h)")
for symbol, data in price_data.items():
change = data.get(class="hl-str">"change_24h", 0)
if change > 5:
report.append(fclass="hl-str">" BULLISH: {symbol} +{change:.1f}% — consider taking profits")
elif change < -5:
report.append(fclass="hl-str">" BEARISH: {symbol} {change:.1f}% — potential buying opportunity")
class=class="hl-str">"hl-comment"># Check if we already hold it
held = any(b[class="hl-str">"symbol"] == symbol for b in balances)
if not held:
recommendations.append({
class="hl-str">"action": class="hl-str">"buy",
class="hl-str">"token": symbol,
class="hl-str">"reason": fclass="hl-str">"Down {change:.1f}% — potential dip buy",
})
else:
report.append(fclass="hl-str">" NEUTRAL: {symbol} {change:+.1f}%")
class=class="hl-str">"hl-comment"># 3. Diversification
report.append(class="hl-str">"\n 3. DIVERSIFICATION")
num_tokens = len(balances)
chains = set(b[class="hl-str">"chain"] for b in balances)
score = min(10, num_tokens * 2 + len(chains))
report.append(fclass="hl-str">" Tokens held: {num_tokens}")
report.append(fclass="hl-str">" Chains used: {len(chains)} ({class="hl-str">', '.join(chains)})")
report.append(fclass="hl-str">" Score: {score}/10")
if num_tokens < 3:
report.append(class="hl-str">" TIP: Consider diversifying into at least 3-5 tokens")
class=class="hl-str">"hl-comment"># 4. Stablecoin ratio
report.append(class="hl-str">"\n 4. STABLECOIN RATIO")
stable_symbols = {class="hl-str">"USDC", class="hl-str">"USDT", class="hl-str">"DAI"}
stable_usd = sum(b[class="hl-str">"usd_value"] for b in balances if b[class="hl-str">"symbol"] in stable_symbols)
stable_pct = (stable_usd / total_usd) * 100 if total_usd > 0 else 0
report.append(fclass="hl-str">" Stablecoins: ${stable_usd:,.2f} ({stable_pct:.1f}%)")
if stable_pct < 10:
report.append(class="hl-str">" WARNING: Low stablecoin allocation (<10%). Consider increasing for risk management.")
recommendations.append({
class="hl-str">"action": class="hl-str">"rebalance",
class="hl-str">"token": class="hl-str">"USDC",
class="hl-str">"reason": class="hl-str">"Stablecoin allocation too low for risk management",
})
elif stable_pct > 60:
report.append(class="hl-str">" NOTE: High stablecoin ratio (>60%). Capital may be underdeployed.")
class=class="hl-str">"hl-comment"># 5. Recommendations
report.append(class="hl-str">"\n 5. RECOMMENDATIONS")
if recommendations:
for i, rec in enumerate(recommendations, 1):
report.append(fclass="hl-str">" {i}. {rec[class="hl-str">'action'].upper()} {rec[class="hl-str">'token']}: {rec[class="hl-str">'reason']}")
if class="hl-str">"target" in rec:
report.append(fclass="hl-str">" → {rec[class="hl-str">'target']}")
else:
report.append(class="hl-str">" No immediate action needed. Portfolio looks balanced.")
report.append(class="hl-str">"")
return class="hl-str">"\n".join(report), recommendations
def main():
api_key = os.environ.get(class="hl-str">"SUWAPPU_API_KEY")
if not api_key:
print(class="hl-str">"Error: Set SUWAPPU_API_KEY environment variable.")
print(class="hl-str">" export SUWAPPU_API_KEY=suwappu_sk_your_api_key")
sys.exit(1)
wallet_address = os.environ.get(class="hl-str">"WALLET_ADDRESS")
if not wallet_address:
print(class="hl-str">"Error: Set WALLET_ADDRESS environment variable.")
print(class="hl-str">" export WALLET_ADDRESS=0xYourWalletAddress")
sys.exit(1)
class=class="hl-str">"hl-comment"># Step 1: Initialize MCP client
print(class="hl-str">"Connecting to Suwappu MCP...")
client = McpClient(api_key)
client.initialize()
class=class="hl-str">"hl-comment"># Step 2: Discover tools
print(class="hl-str">"\nDiscovering tools...")
tools = client.list_tools()
print(fclass="hl-str">"Found {len(tools)} tools:")
for tool in tools:
desc = tool.get(class="hl-str">"description", class="hl-str">"No description")
print(fclass="hl-str">" - {tool[class="hl-str">'name']}: {desc}")
class=class="hl-str">"hl-comment"># Step 3: Fetch portfolio
print(fclass="hl-str">"\nFetching portfolio for {wallet_address[:10]}...{wallet_address[-6:]}...")
portfolio = client.call_tool(class="hl-str">"get_portfolio", {
class="hl-str">"wallet_address": wallet_address,
})
if not portfolio.get(class="hl-str">"balances"):
print(class="hl-str">"Portfolio is empty. Fund your wallet first.")
sys.exit(0)
class=class="hl-str">"hl-comment"># Print holdings
print(fclass="hl-str">"\nPortfolio value: ${portfolio[class="hl-str">'total_usd']:,.2f}")
for bal in portfolio[class="hl-str">"balances"]:
pct = (bal[class="hl-str">"usd_value"] / portfolio[class="hl-str">"total_usd"]) * 100
print(fclass="hl-str">" {bal[class="hl-str">'symbol']:>6} | {bal[class="hl-str">'balance']:>12} | ${bal[class="hl-str">'usd_value']:>10,.2f} | {pct:>5.1f}%")
class=class="hl-str">"hl-comment"># Step 4: Fetch prices
symbols = [b[class="hl-str">"symbol"] for b in portfolio[class="hl-str">"balances"]]
print(fclass="hl-str">"\nFetching prices for {class="hl-str">', '.join(symbols)}...")
prices = client.call_tool(class="hl-str">"get_prices", {
class="hl-str">"symbols": class="hl-str">",".join(symbols),
})
price_data = prices.get(class="hl-str">"prices", prices)
for symbol, data in price_data.items():
change = data.get(class="hl-str">"change_24h", 0)
arrow = class="hl-str">"▲" if change > 0 else class="hl-str">"▼" if change < 0 else class="hl-str">"─"
print(fclass="hl-str">" {symbol}: ${data[class="hl-str">'usd']:,.2f} {arrow} {change:+.1f}%")
class=class="hl-str">"hl-comment"># Step 5: Fetch supported chains
print(class="hl-str">"\nFetching supported chains...")
chains = client.call_tool(class="hl-str">"list_chains")
class=class="hl-str">"hl-comment"># Step 6: Generate analysis
print(class="hl-str">"\nAnalyzing portfolio...")
ai_result = analyze_with_ai(portfolio, price_data, chains)
if ai_result:
print(class="hl-str">"\n" + ai_result)
recommendations = [] class=class="hl-str">"hl-comment"># AI provides its own recommendations
else:
print(class="hl-str">"\n(No AI key found — using rule-based analysis)")
report, recommendations = rule_based_analysis(portfolio, price_data)
print(report)
class=class="hl-str">"hl-comment"># Step 7: Get quotes for recommendations
if recommendations:
print(class="hl-str">"\nFetching quotes for recommended trades...")
for rec in recommendations:
if rec[class="hl-str">"action"] == class="hl-str">"sell":
class=class="hl-str">"hl-comment"># Show quote for selling some of the overweight token
try:
quote = client.call_tool(class="hl-str">"get_quote", {
class="hl-str">"from_token": rec[class="hl-str">"token"],
class="hl-str">"to_token": class="hl-str">"USDC",
class="hl-str">"amount": class="hl-str">"0.1", class=class="hl-str">"hl-comment"># Small sample amount
class="hl-str">"chain": class="hl-str">"ethereum",
})
print(fclass="hl-str">" Sample quote: 0.1 {rec[class="hl-str">'token']} → {quote.get(class="hl-str">'to_amount', quote.get(class="hl-str">'amount_out', class="hl-str">'?'))} USDC")
except Exception as e:
print(fclass="hl-str">" Could not quote {rec[class="hl-str">'token']}: {e}")
elif rec[class="hl-str">"action"] == class="hl-str">"buy":
try:
quote = client.call_tool(class="hl-str">"get_quote", {
class="hl-str">"from_token": class="hl-str">"USDC",
class="hl-str">"to_token": rec[class="hl-str">"token"],
class="hl-str">"amount": class="hl-str">"100", class=class="hl-str">"hl-comment"># $100 sample
class="hl-str">"chain": class="hl-str">"ethereum",
})
print(fclass="hl-str">" Sample quote: 100 USDC → {quote.get(class="hl-str">'to_amount', quote.get(class="hl-str">'amount_out', class="hl-str">'?'))} {rec[class="hl-str">'token']}")
except Exception as e:
print(fclass="hl-str">" Could not quote {rec[class="hl-str">'token']}: {e}")
print(class="hl-str">"\nDone. This is not financial advice — always do your own research.")
if __name__ == class="hl-str">"__main__":
main()
Running the Python Version
-str">"hl-comment"># Install dependencies
-kw">pip install requests
-str">"hl-comment"># Required
-kw">export SUWAPPU_API_KEY=suwappu_sk_your_api_key
-kw">export WALLET_ADDRESS=0xYourWalletAddress
-str">"hl-comment"># Optional: enable AI analysis (use one or neither)
-kw">export OPENAI_API_KEY=sk-your-openai-key
-kw">export ANTHROPIC_API_KEY=sk-ant-your-anthropic-key
-str">"hl-comment"># Run the advisor
python mcp_portfolio_advisor.py
---
TypeScript Version
class=class="hl-str">"hl-comment">#!/usr/bin/env npx tsx;/**
* Suwappu MCP Portfolio Advisor — TypeScript
* Custom MCP client that discovers tools, fetches data, and generates investment advice.
*/
const MCP_URL = class="hl-str">"https:class="hl-commentclass="hl-str">">//api.suwappu.bot/mcp"; class McpClient {private apiKey: string;
private requestId = 0;
tools: Array<{ name: string; description?: string; inputSchema?: unknown }> = [];
constructor(apiKey: string) {
this.apiKey = apiKey;
}
private nextId(): number {
return ++this.requestId;
}
private async send(method: string, params: Record<string, unknown> = {}) {
const response = await fetch(MCP_URL, {
method: class="hl-str">"POST",
headers: {
class="hl-str">"Content-Type": class="hl-str">"application/json",
Authorization:
Bearer ${this.apiKey},},
body: JSON.stringify({
jsonrpc: class="hl-str">"2.0",
id: this.nextId(),
method,
params,
}),
});
if (!response.ok) {
throw new Error(
HTTP ${response.status}: ${await response.text()});}
const data = await response.json();
if (data.error) {
throw new Error(
MCP error ${data.error.code}: ${data.error.message});}
return data.result;
}
async initialize() {
const result = await this.send(class="hl-str">"initialize");
const server = result.serverInfo;
console.log(
Connected to ${server.name} v${server.version});console.log(
Protocol: ${result.protocolVersion});return result;
}
async listTools() {
const result = await this.send(class="hl-str">"tools/list");
this.tools = result.tools ?? [];
return this.tools;
}
async callTool(name: string, args: Record<string, unknown> = {}): Promise<any> {
const result = await this.send(class="hl-str">"tools/call", { name, arguments: args });
const content = result.content ?? [];
for (const part of content) {
if (part.type === class="hl-str">"text") {
try {
return JSON.parse(part.text);
} catch {
return part.text;
}
}
}
return content;
}
}
interface Balance {symbol: string;
chain: string;
balance: string;
usd_value: number;
}
interface PriceData {usd: number;
change_24h: number;
}
async function analyzeWithAi(portfolio: { balances: Balance[]; total_usd: number },
prices: Record<string, PriceData>,
chains: unknown
): Promise<string | null> {
const openaiKey = process.env.OPENAI_API_KEY;
const anthropicKey = process.env.ANTHROPIC_API_KEY;
const prompt =
You are a crypto portfolio advisor. Analyze this portfolio and provide actionable recommendations.Portfolio Holdings:
${JSON.stringify(portfolio, null, 2)}
Current Prices (with 24h change):
${JSON.stringify(prices, null, 2)}
Supported Chains:
${JSON.stringify(chains, null, 2)}
Analyze for:
1. Concentration risk (any single token >50% of portfolio?) 2. Momentum signals (tokens with >5% 24h change) 3. Diversification score (how many tokens, how spread across chains) 4. Stablecoin ratio (is there enough stable allocation for risk management?)Provide 2-3 specific trade recommendations with reasoning. Format as a clear report.
if (openaiKey) {
const res = await fetch(class="hl-str">"https:class="hl-commentclass="hl-str">">//api.openai.com/v1/chat/completions", {
method: class="hl-str">"POST",
headers: { Authorization:
Bearer ${openaiKey}, class="hl-str">"Content-Type": class="hl-str">"application/json" },body: JSON.stringify({
model: class="hl-str">"gpt-4o",
messages: [{ role: class="hl-str">"user", content: prompt }],
temperature: 0.3,
}),
});
if (!res.ok) throw new Error(
OpenAI error: ${await res.text()});const data = await res.json();
return data.choices[0].message.content;
}
if (anthropicKey) {
const res = await fetch(class="hl-str">"https:class="hl-commentclass="hl-str">">//api.anthropic.com/v1/messages", {
method: class="hl-str">"POST",
headers: {
class="hl-str">"x-api-key": anthropicKey,
class="hl-str">"anthropic-version": class="hl-str">"2023-06-01",
class="hl-str">"Content-Type": class="hl-str">"application/json",
},
body: JSON.stringify({
model: class="hl-str">"claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [{ role: class="hl-str">"user", content: prompt }],
}),
});
if (!res.ok) throw new Error(
Anthropic error: ${await res.text()});const data = await res.json();
return data.content[0].text;
}
return null;
}
interface Recommendation {action: string;
token: string;
reason: string;
target?: string;
}
function ruleBasedAnalysis(portfolio: { balances: Balance[]; total_usd: number },
prices: Record<string, PriceData>
): { report: string; recommendations: Recommendation[] } {
const { balances, total_usd } = portfolio;
const recommendations: Recommendation[] = [];
if (total_usd === 0) {
return { report: class="hl-str">"Portfolio is empty. Fund your wallet to get started.", recommendations: [] };
}
const lines: string[] = [];
lines.push(class="hl-str">"=".repeat(55));
lines.push(class="hl-str">" PORTFOLIO ADVISORY REPORT");
lines.push(class="hl-str">"=".repeat(55));
class=class="hl-str">"hl-comment">// 1. Concentration risk
lines.push(class="hl-str">"\n 1. CONCENTRATION RISK");
for (const bal of balances) {
const pct = (bal.usd_value / total_usd) * 100;
if (pct > 50) {
lines.push(
WARNING: ${bal.symbol} is ${pct.toFixed(1)}% of portfolio (>50%));recommendations.push({
action: class="hl-str">"sell",
token: bal.symbol,
reason:
Over-concentrated at ${pct.toFixed(1)}%,target: class="hl-str">"Reduce to <40% by selling into USDC or diversifying",
});
} else if (pct > 30) {
lines.push(
WATCH: ${bal.symbol} at ${pct.toFixed(1)}% — approaching concentration limit);} else {
lines.push(
OK: ${bal.symbol} at ${pct.toFixed(1)}%);}
}
class=class="hl-str">"hl-comment">// 2. Momentum signals
lines.push(class="hl-str">"\n 2. MOMENTUM SIGNALS (24h)");
for (const [symbol, data] of Object.entries(prices)) {
const change = data.change_24h ?? 0;
if (change > 5) {
lines.push(
BULLISH: ${symbol} +${change.toFixed(1)}% — consider taking profits);} else if (change < -5) {
lines.push(
BEARISH: ${symbol} ${change.toFixed(1)}% — potential buying opportunity);const held = balances.some((b) => b.symbol === symbol);
if (!held) {
recommendations.push({
action: class="hl-str">"buy",
token: symbol,
reason:
Down ${change.toFixed(1)}% — potential dip buy,});
}
} else {
lines.push(
NEUTRAL: ${symbol} ${change >= 0 ? class="hl-str">"+" : class="hl-str">""}${change.toFixed(1)}%);}
}
class=class="hl-str">"hl-comment">// 3. Diversification
lines.push(class="hl-str">"\n 3. DIVERSIFICATION");
const chains = new Set(balances.map((b) => b.chain));
const score = Math.min(10, balances.length * 2 + chains.size);
lines.push(
Tokens held: ${balances.length});lines.push(
Chains used: ${chains.size} (${[...chains].join(class="hl-str">", ")}));lines.push(
Score: ${score}/10);if (balances.length < 3) {
lines.push(class="hl-str">" TIP: Consider diversifying into at least 3-5 tokens");
}
class=class="hl-str">"hl-comment">// 4. Stablecoin ratio
lines.push(class="hl-str">"\n 4. STABLECOIN RATIO");
const stableSymbols = new Set([class="hl-str">"USDC", class="hl-str">"USDT", class="hl-str">"DAI"]);
const stableUsd = balances
.filter((b) => stableSymbols.has(b.symbol))
.reduce((sum, b) => sum + b.usd_value, 0);
const stablePct = (stableUsd / total_usd) * 100;
lines.push(
Stablecoins: $${stableUsd.toLocaleString(class="hl-str">"en-US", { minimumFractionDigits: 2 })} (${stablePct.toFixed(1)}%));if (stablePct < 10) {
lines.push(class="hl-str">" WARNING: Low stablecoin allocation (<10%). Consider increasing for risk management.");
recommendations.push({
action: class="hl-str">"rebalance",
token: class="hl-str">"USDC",
reason: class="hl-str">"Stablecoin allocation too low for risk management",
});
} else if (stablePct > 60) {
lines.push(class="hl-str">" NOTE: High stablecoin ratio (>60%). Capital may be underdeployed.");
}
class=class="hl-str">"hl-comment">// 5. Recommendations
lines.push(class="hl-str">"\n 5. RECOMMENDATIONS");
if (recommendations.length > 0) {
recommendations.forEach((rec, i) => {
lines.push(
${i + 1}. ${rec.action.toUpperCase()} ${rec.token}: ${rec.reason});if (rec.target) lines.push(
→ ${rec.target});});
} else {
lines.push(class="hl-str">" No immediate action needed. Portfolio looks balanced.");
}
lines.push(class="hl-str">"");
return { report: lines.join(class="hl-str">"\n"), recommendations };
}
async function main() {const apiKey = process.env.SUWAPPU_API_KEY;
if (!apiKey) {
console.error(class="hl-str">"Error: Set SUWAPPU_API_KEY environment variable.");
process.exit(1);
}
const walletAddress = process.env.WALLET_ADDRESS;
if (!walletAddress) {
console.error(class="hl-str">"Error: Set WALLET_ADDRESS environment variable.");
process.exit(1);
}
class=class="hl-str">"hl-comment">// Step 1: Initialize MCP client
console.log(class="hl-str">"Connecting to Suwappu MCP...");
const client = new McpClient(apiKey);
await client.initialize();
class=class="hl-str">"hl-comment">// Step 2: Discover tools
console.log(class="hl-str">"\nDiscovering tools...");
const tools = await client.listTools();
console.log(
Found ${tools.length} tools:);for (const tool of tools) {
console.log(
- ${tool.name}: ${tool.description ?? class="hl-str">"No description"});}
class=class="hl-str">"hl-comment">// Step 3: Fetch portfolio
console.log(
\nFetching portfolio for ${walletAddress.slice(0, 10)}...${walletAddress.slice(-6)}...);const portfolio = await client.callTool(class="hl-str">"get_portfolio", { wallet_address: walletAddress });
if (!portfolio.balances?.length) {
console.log(class="hl-str">"Portfolio is empty. Fund your wallet first.");
process.exit(0);
}
console.log(
\nPortfolio value: $${portfolio.total_usd.toLocaleString(class="hl-str">"en-US", { minimumFractionDigits: 2 })});for (const bal of portfolio.balances) {
const pct = (bal.usd_value / portfolio.total_usd) * 100;
console.log(
${bal.symbol.padStart(6)} | ${bal.balance.padStart(12)} | $${bal.usd_value.toFixed(2).padStart(10)} | ${pct.toFixed(1).padStart(5)}%);
}
class=class="hl-str">"hl-comment">// Step 4: Fetch prices
const symbols = portfolio.balances.map((b: Balance) => b.symbol);
console.log(
\nFetching prices for ${symbols.join(class="hl-str">", ")}...);const priceResult = await client.callTool(class="hl-str">"get_prices", { symbols: symbols.join(class="hl-str">",") });
const priceData: Record<string, PriceData> = priceResult.prices ?? priceResult;
for (const [symbol, data] of Object.entries(priceData)) {
const change = data.change_24h ?? 0;
const arrow = change > 0 ? class="hl-str">"▲" : change < 0 ? class="hl-str">"▼" : class="hl-str">"─";
console.log(
${symbol}: $${data.usd.toLocaleString(class="hl-str">"en-US", { minimumFractionDigits: 2 })} ${arrow} ${change >= 0 ? class="hl-str">"+" : class="hl-str">""}${change.toFixed(1)}%);}
class=class="hl-str">"hl-comment">// Step 5: Fetch chains
console.log(class="hl-str">"\nFetching supported chains...");
const chains = await client.callTool(class="hl-str">"list_chains");
class=class="hl-str">"hl-comment">// Step 6: Generate analysis
console.log(class="hl-str">"\nAnalyzing portfolio...");
const aiResult = await analyzeWithAi(portfolio, priceData, chains);
let recommendations: Recommendation[] = [];
if (aiResult) {
console.log(class="hl-str">"\n" + aiResult);
} else {
console.log(class="hl-str">"\n(No AI key found — using rule-based analysis)");
const analysis = ruleBasedAnalysis(portfolio, priceData);
console.log(analysis.report);
recommendations = analysis.recommendations;
}
class=class="hl-str">"hl-comment">// Step 7: Get quotes for recommendations
if (recommendations.length > 0) {
console.log(class="hl-str">"\nFetching quotes for recommended trades...");
for (const rec of recommendations) {
if (rec.action === class="hl-str">"sell") {
try {
const quote = await client.callTool(class="hl-str">"get_quote", {
from_token: rec.token,
to_token: class="hl-str">"USDC",
amount: class="hl-str">"0.1",
chain: class="hl-str">"ethereum",
});
console.log(
Sample quote: 0.1 ${rec.token} → ${quote.to_amount ?? quote.amount_out ?? class="hl-str">"?"} USDC);} catch (e) {
console.log(
Could not quote ${rec.token}: ${e});}
} else if (rec.action === class="hl-str">"buy") {
try {
const quote = await client.callTool(class="hl-str">"get_quote", {
from_token: class="hl-str">"USDC",
to_token: rec.token,
amount: class="hl-str">"100",
chain: class="hl-str">"ethereum",
});
console.log(
Sample quote: 100 USDC → ${quote.to_amount ?? quote.amount_out ?? class="hl-str">"?"} ${rec.token});} catch (e) {
console.log(
Could not quote ${rec.token}: ${e});}
}
}
}
console.log(class="hl-str">"\nDone. This is not financial advice — always do your own research.");
}
main().catch(console.error);
Running the TypeScript Version
-str">"hl-comment"># Install tsx for running TypeScript directly
-kw">npm install -g tsx
-str">"hl-comment"># Required
-kw">export SUWAPPU_API_KEY=suwappu_sk_your_api_key
-kw">export WALLET_ADDRESS=0xYourWalletAddress
-str">"hl-comment"># Optional: enable AI analysis (use one or neither)
-kw">export OPENAI_API_KEY=sk-your-openai-key
-kw">export ANTHROPIC_API_KEY=sk-ant-your-anthropic-key
-str">"hl-comment"># Run the advisor
npx tsx mcp_portfolio_advisor.ts
---
Example Output
Connecting to Suwappu MCP...
Connected to suwappu v0.4.0
Protocol: 2024-11-05
Discovering tools...
Found 6 tools:
- get_quote: Get a swap quote for a token pair
- execute_swap: Execute a previously obtained quote
- get_portfolio: Check token balances for a wallet
- get_prices: Get current prices for one or more tokens
- list_chains: List all supported blockchain networks
- list_tokens: Search and list available tokens
Fetching portfolio for 0xd8dA6BF2...96045...
Portfolio value: $48,250.75
ETH | 12.500 | $43,755.25 | 90.7%
USDC | 3,200.00 | $3,200.00 | 6.6%
DAI | 1,295.50 | $1,295.50 | 2.7%
Fetching prices for ETH, USDC, DAI...
ETH: $3,500.42 ▲ +2.5%
USDC: $1.00 ─ +0.0%
DAI: $1.00 ─ +0.0%
Analyzing portfolio...
(No AI key found — using rule-based analysis)
=======================================================
PORTFOLIO ADVISORY REPORT
=======================================================
1. CONCENTRATION RISK
WARNING: ETH is 90.7% of portfolio (>50%)
OK: USDC at 6.6%
OK: DAI at 2.7%
2. MOMENTUM SIGNALS (24h)
NEUTRAL: ETH +2.5%
NEUTRAL: USDC +0.0%
NEUTRAL: DAI +0.0%
3. DIVERSIFICATION
Tokens held: 3
Chains used: 1 (ethereum)
Score: 7/10
TIP: Consider diversifying into at least 3-5 tokens
4. STABLECOIN RATIO
Stablecoins: $4,495.50 (9.3%)
WARNING: Low stablecoin allocation (<10%).
5. RECOMMENDATIONS
1. SELL ETH: Over-concentrated at 90.7%
→ Reduce to <40% by selling into USDC or diversifying
2. REBALANCE USDC: Stablecoin allocation too low
Fetching quotes for recommended trades...
Sample quote: 0.1 ETH → 349.50 USDC
Done. This is not financial advice — always do your own research.
---
Customization Tips
Add More Analysis Rules
Extend the rule-based engine with additional checks:
class=class="hl-str">"hl-comment"># Check for tokens with very small positions (dust)
for bal in balances:
if bal[class="hl-str">"usd_value"] < 10:
recommendations.append({
class="hl-str">"action": class="hl-str">"sell",
class="hl-str">"token": bal[class="hl-str">"symbol"],
class="hl-str">"reason": fclass="hl-str">"Dust position (${bal[class="hl-str">'usd_value']:.2f}) — consolidate or remove",
})
Use a Different AI Model
Swap the model by changing the model parameter:
class=class="hl-str">"hl-comment"># Use a cheaper/faster model
class="hl-str">"model": class="hl-str">"gpt-4o-mini"
class=class="hl-str">"hl-comment"># Or use Claude Haiku for speed
class="hl-str">"model": class="hl-str">"claude-haiku-4-5-20251001"
Schedule Regular Reports
Run the advisor on a cron schedule to get daily portfolio insights:
-str">"hl-comment"># Run every morning at 8am UTC
0 8 * * * SUWAPPU_API_KEY=suwappu_sk_... WALLET_ADDRESS=0x... python /path/to/mcp_portfolio_advisor.py >> /var/log/advisor.log 2>&1
Auto-Execute Recommendations
Add a --execute flag to automatically act on recommendations:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(class="hl-str">"--execute", action=class="hl-str">"store_true", help=class="hl-str">"Auto-execute recommended trades")
args = parser.parse_args()
if args.execute and recommendations:
for rec in recommendations:
class=class="hl-str">"hl-comment"># Execute via MCP tools instead of just quoting
quote = client.call_tool(class="hl-str">"get_quote", {...})
result = client.call_tool(class="hl-str">"execute_swap", {
class="hl-str">"quote_id": quote[class="hl-str">"quote_id"],
class="hl-str">"wallet_address": wallet_address,
})
print(fclass="hl-str">"Executed: {result}")