Skip to content

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}")