@wealthville/mcp-server
Advanced tools
+64
| #!/usr/bin/env node | ||
| /** | ||
| * Wealthville MCP server — Streamable HTTP transport for hosted/remote | ||
| * deployments (e.g. Smithery, or self-hosted at https://<host>/mcp). | ||
| * | ||
| * Stateless: a fresh McpServer + transport is created per request, so there is | ||
| * no session state to leak between callers and the process scales horizontally. | ||
| * All four tools are read-only, so statelessness costs nothing. | ||
| * | ||
| * Per-request config (optional) is read from, in order: | ||
| * - ?config=<base64 JSON> (Smithery convention: { wealthvilleApiKey, wealthvilleApiUrl }) | ||
| * - ?wealthvilleApiKey= / ?wealthvilleApiUrl= (flat query params) | ||
| * - x-api-key header | ||
| * - falling back to WEALTHVILLE_API_KEY / WEALTHVILLE_API_URL env | ||
| * | ||
| * Env: | ||
| * PORT — listen port (default 8080) | ||
| */ | ||
| import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; | ||
| import express from 'express'; | ||
| import { buildServer, VERSION } from './server.js'; | ||
| const PORT = Number(process.env.PORT || 8080); | ||
| function parseConfig(req) { | ||
| const q = req.query; | ||
| let cfg = {}; | ||
| if (typeof q.config === 'string') { | ||
| try { | ||
| cfg = JSON.parse(Buffer.from(q.config, 'base64').toString('utf8')); | ||
| } | ||
| catch { /* ignore malformed config */ } | ||
| } | ||
| const apiKey = cfg.wealthvilleApiKey ?? q.wealthvilleApiKey ?? req.headers['x-api-key']; | ||
| const apiUrl = cfg.wealthvilleApiUrl ?? q.wealthvilleApiUrl; | ||
| return { apiKey, apiUrl }; | ||
| } | ||
| const app = express(); | ||
| app.use(express.json()); | ||
| // Liveness probe for hosting platforms. | ||
| app.get('/health', (_req, res) => { | ||
| res.json({ ok: true, name: 'wealthville', version: VERSION }); | ||
| }); | ||
| // Streamable HTTP endpoint — stateless request/response. | ||
| app.post('/mcp', async (req, res) => { | ||
| const server = buildServer(parseConfig(req)); | ||
| const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); | ||
| res.on('close', () => { void transport.close(); void server.close(); }); | ||
| try { | ||
| await server.connect(transport); | ||
| await transport.handleRequest(req, res, req.body); | ||
| } | ||
| catch { | ||
| if (!res.headersSent) { | ||
| res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null }); | ||
| } | ||
| } | ||
| }); | ||
| // Stateless server: no server-initiated streams or session teardown. | ||
| const methodNotAllowed = (_req, res) => res.status(405).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed (stateless server)' }, id: null }); | ||
| app.get('/mcp', methodNotAllowed); | ||
| app.delete('/mcp', methodNotAllowed); | ||
| app.listen(PORT, () => { | ||
| // Log to stderr so it never pollutes any stdout JSON. | ||
| console.error(`Wealthville MCP (Streamable HTTP) v${VERSION} listening on :${PORT} at POST /mcp`); | ||
| }); |
| /** | ||
| * Wealthville MCP server core — builds the McpServer with all four read-only | ||
| * tools. Shared by both transports: `index.ts` (stdio, local/npx) and | ||
| * `http.ts` (Streamable HTTP, hosted deployments like Smithery). | ||
| * | ||
| * Config resolution order for each request/process: explicit `config` arg → | ||
| * environment variables → defaults. This lets the HTTP transport pass per-request | ||
| * config while the stdio transport relies on env. | ||
| */ | ||
| import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | ||
| import { createRequire } from 'node:module'; | ||
| import { z } from 'zod'; | ||
| // Single source of truth for the version: read package.json at runtime so the | ||
| // serverInfo reported over MCP always matches the published npm version. dist/ | ||
| // sits one level below package.json in the tarball, and npm always includes | ||
| // package.json, so '../package.json' resolves in the installed package. | ||
| const require = createRequire(import.meta.url); | ||
| export const VERSION = require('../package.json').version; | ||
| const DEFAULT_BASE_URL = 'https://wealthville.net'; | ||
| const DISCLAIMER = 'Wealthville scores are a data product, not financial advice. ' | ||
| + 'Methodology: https://www.wealthville.net/learn/wealthville-score — ' | ||
| + 'live track record (misses included): https://www.wealthville.net/track-record'; | ||
| // All four tools are read-only GET wrappers over the public Wealthville API — no | ||
| // state changes and safe to retry. Surfaced as MCP annotation hints so Glama (and | ||
| // any client) can flag them non-destructive / read-only. | ||
| const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }; | ||
| /** Every tool returns JSON plus the methodology/disclaimer line so agents repeat it. */ | ||
| function toResult(data) { | ||
| return { | ||
| content: [ | ||
| { type: 'text', text: JSON.stringify(data, null, 2) }, | ||
| { type: 'text', text: DISCLAIMER }, | ||
| ], | ||
| }; | ||
| } | ||
| /** Build a fully-configured McpServer instance. Safe to call once per stdio process or once per HTTP request. */ | ||
| export function buildServer(config = {}) { | ||
| const baseUrl = (config.apiUrl || process.env.WEALTHVILLE_API_URL || DEFAULT_BASE_URL).replace(/\/$/, ''); | ||
| const apiKey = config.apiKey || process.env.WEALTHVILLE_API_KEY; | ||
| async function apiGet(path) { | ||
| const headers = { accept: 'application/json' }; | ||
| if (apiKey) | ||
| headers['x-api-key'] = apiKey; | ||
| const res = await fetch(`${baseUrl}${path}`, { headers }); | ||
| if (!res.ok) { | ||
| throw new Error(`Wealthville API ${res.status} for ${path}${res.status === 429 ? ' (rate limited — retry shortly or set WEALTHVILLE_API_KEY)' : ''}`); | ||
| } | ||
| return res.json(); | ||
| } | ||
| const server = new McpServer({ name: 'wealthville', version: VERSION }); | ||
| server.tool('get_pool_score', 'Get the Wealthville verdict (ENTER/HOLD/EXIT/AVOID) and scores (enter/hold/exit + composite ' | ||
| + 'Wealthville Score, 0-100) for one liquidity pool. Use before recommending or opening any LP position. ' | ||
| + 'Accepts a Solana pool address (base58) or an EVM 0x address / DefiLlama pool UUID.', { pool_address: z.string().min(8).describe('Pool address: Solana base58, EVM 0x, or DefiLlama UUID') }, READ_ONLY, async ({ pool_address }) => toResult(await apiGet(`/api/v1/scores/${encodeURIComponent(pool_address)}`))); | ||
| server.tool('get_top_pools', 'List liquidity pools ranked by composite Wealthville Score (0-100), freshly scored within the last ' | ||
| + '6 hours. Good for "what are the best pools right now?" questions.', { | ||
| limit: z.number().int().min(1).max(100).optional().describe('How many pools (default 25)'), | ||
| chain: z.string().optional().describe('"solana" (default), "evm" (all EVM chains), or one EVM chain e.g. "ethereum", "base"'), | ||
| }, READ_ONLY, async ({ limit, chain }) => { | ||
| const params = new URLSearchParams(); | ||
| if (limit) | ||
| params.set('limit', String(limit)); | ||
| if (chain) | ||
| params.set('chain', chain); | ||
| const qs = params.toString(); | ||
| return toResult(await apiGet(`/api/v1/scores/top${qs ? `?${qs}` : ''}`)); | ||
| }); | ||
| server.tool('get_track_record', 'Get Wealthville\'s live signal track record: per-action hit rates, IL-adjusted 7-day PnL, and recent ' | ||
| + 'resolved signals — misses included (the ledger is immutable at publish time). Use when asked whether ' | ||
| + 'Wealthville scores can be trusted, or for the system\'s recent performance.', { days: z.number().int().min(7).max(90).optional().describe('Window in days (default 30)') }, READ_ONLY, async ({ days }) => toResult(await apiGet(`/api/v1/track-record${days ? `?days=${days}` : ''}`))); | ||
| server.tool('get_signals_feed', 'Get the latest published Wealthville signals (ENTER/EXIT/RISK_OFF calls with narrative and confidence). ' | ||
| + 'Use for "any new LP signals?" questions.', { limit: z.number().int().min(1).max(50).optional().describe('How many signals (default 20)') }, READ_ONLY, async ({ limit }) => toResult(await apiGet(`/api/v1/signals/feed${limit ? `?limit=${limit}` : ''}`))); | ||
| return server; | ||
| } |
+11
-65
| #!/usr/bin/env node | ||
| /** | ||
| * Wealthville MCP server — exposes the public Wealthville data API | ||
| * (https://wealthville.net/developers) as MCP tools so AI assistants can | ||
| * answer "should I LP into this pool?" with a scored, track-recorded answer. | ||
| * Wealthville MCP server — stdio transport (local / npx). | ||
| * | ||
| * Read-only; wraps four public HTTP endpoints. Optional env: | ||
| * Exposes the public Wealthville data API (https://wealthville.net/developers) | ||
| * as MCP tools so AI assistants can answer "should I LP into this pool?" with a | ||
| * scored, track-recorded answer. Read-only; wraps four public GET endpoints. | ||
| * | ||
| * Optional env: | ||
| * WEALTHVILLE_API_KEY — partner key (higher rate limit), sent as x-api-key | ||
| * WEALTHVILLE_API_URL — override base URL (default https://wealthville.net) | ||
| * | ||
| * For hosted/remote deployments (HTTP), see ./http.ts. | ||
| */ | ||
| import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | ||
| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; | ||
| import { createRequire } from 'node:module'; | ||
| import { z } from 'zod'; | ||
| // Single source of truth for the version: read package.json at runtime so the | ||
| // serverInfo reported over MCP always matches the published npm version. dist/ | ||
| // sits one level below package.json in the tarball, and npm always includes | ||
| // package.json, so '../package.json' resolves in the installed package. | ||
| const require = createRequire(import.meta.url); | ||
| const { version: VERSION } = require('../package.json'); | ||
| const BASE_URL = (process.env.WEALTHVILLE_API_URL || 'https://wealthville.net').replace(/\/$/, ''); | ||
| const API_KEY = process.env.WEALTHVILLE_API_KEY; | ||
| const DISCLAIMER = 'Wealthville scores are a data product, not financial advice. ' | ||
| + 'Methodology: https://www.wealthville.net/learn/wealthville-score — ' | ||
| + 'live track record (misses included): https://www.wealthville.net/track-record'; | ||
| async function apiGet(path) { | ||
| const headers = { accept: 'application/json' }; | ||
| if (API_KEY) | ||
| headers['x-api-key'] = API_KEY; | ||
| const res = await fetch(`${BASE_URL}${path}`, { headers }); | ||
| if (!res.ok) { | ||
| throw new Error(`Wealthville API ${res.status} for ${path}${res.status === 429 ? ' (rate limited — retry shortly or set WEALTHVILLE_API_KEY)' : ''}`); | ||
| } | ||
| return res.json(); | ||
| } | ||
| /** Every tool returns JSON plus the methodology/disclaimer line so agents repeat it. */ | ||
| function toResult(data) { | ||
| return { | ||
| content: [ | ||
| { type: 'text', text: JSON.stringify(data, null, 2) }, | ||
| { type: 'text', text: DISCLAIMER }, | ||
| ], | ||
| }; | ||
| } | ||
| // All four tools are read-only GET wrappers over the public Wealthville API — no | ||
| // state changes and safe to retry. Surfaced as MCP annotation hints so Glama (and | ||
| // any client) can flag them non-destructive / read-only. | ||
| const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }; | ||
| const server = new McpServer({ name: 'wealthville', version: VERSION }); | ||
| server.tool('get_pool_score', 'Get the Wealthville verdict (ENTER/HOLD/EXIT/AVOID) and scores (enter/hold/exit + composite ' | ||
| + 'Wealthville Score, 0-100) for one liquidity pool. Use before recommending or opening any LP position. ' | ||
| + 'Accepts a Solana pool address (base58) or an EVM 0x address / DefiLlama pool UUID.', { pool_address: z.string().min(8).describe('Pool address: Solana base58, EVM 0x, or DefiLlama UUID') }, READ_ONLY, async ({ pool_address }) => toResult(await apiGet(`/api/v1/scores/${encodeURIComponent(pool_address)}`))); | ||
| server.tool('get_top_pools', 'List liquidity pools ranked by composite Wealthville Score (0-100), freshly scored within the last ' | ||
| + '6 hours. Good for "what are the best pools right now?" questions.', { | ||
| limit: z.number().int().min(1).max(100).optional().describe('How many pools (default 25)'), | ||
| chain: z.string().optional().describe('"solana" (default), "evm" (all EVM chains), or one EVM chain e.g. "ethereum", "base"'), | ||
| }, READ_ONLY, async ({ limit, chain }) => { | ||
| const params = new URLSearchParams(); | ||
| if (limit) | ||
| params.set('limit', String(limit)); | ||
| if (chain) | ||
| params.set('chain', chain); | ||
| const qs = params.toString(); | ||
| return toResult(await apiGet(`/api/v1/scores/top${qs ? `?${qs}` : ''}`)); | ||
| }); | ||
| server.tool('get_track_record', 'Get Wealthville\'s live signal track record: per-action hit rates, IL-adjusted 7-day PnL, and recent ' | ||
| + 'resolved signals — misses included (the ledger is immutable at publish time). Use when asked whether ' | ||
| + 'Wealthville scores can be trusted, or for the system\'s recent performance.', { days: z.number().int().min(7).max(90).optional().describe('Window in days (default 30)') }, READ_ONLY, async ({ days }) => toResult(await apiGet(`/api/v1/track-record${days ? `?days=${days}` : ''}`))); | ||
| server.tool('get_signals_feed', 'Get the latest published Wealthville signals (ENTER/EXIT/RISK_OFF calls with narrative and confidence). ' | ||
| + 'Use for "any new LP signals?" questions.', { limit: z.number().int().min(1).max(50).optional().describe('How many signals (default 20)') }, READ_ONLY, async ({ limit }) => toResult(await apiGet(`/api/v1/signals/feed${limit ? `?limit=${limit}` : ''}`))); | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| import { buildServer } from './server.js'; | ||
| const server = buildServer(); | ||
| await server.connect(new StdioServerTransport()); |
+8
-4
| { | ||
| "name": "@wealthville/mcp-server", | ||
| "mcpName": "io.github.amitesh-m/wealthville", | ||
| "version": "0.1.2", | ||
| "version": "0.1.3", | ||
| "description": "MCP server exposing Wealthville pool scores, verdicts, and the live track record to AI assistants", | ||
@@ -9,3 +9,4 @@ "license": "MIT", | ||
| "bin": { | ||
| "wealthville-mcp": "dist/index.js" | ||
| "wealthville-mcp": "dist/index.js", | ||
| "wealthville-mcp-http": "dist/http.js" | ||
| }, | ||
@@ -18,4 +19,5 @@ "main": "dist/index.js", | ||
| "scripts": { | ||
| "build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"", | ||
| "start": "node dist/index.js" | ||
| "build": "tsc -p tsconfig.json && node -e \"const fs=require('fs');['dist/index.js','dist/http.js'].forEach(f=>fs.chmodSync(f,0o755))\"", | ||
| "start": "node dist/index.js", | ||
| "start:http": "node dist/http.js" | ||
| }, | ||
@@ -33,5 +35,7 @@ "keywords": [ | ||
| "@modelcontextprotocol/sdk": "^1.12.0", | ||
| "express": "^4.21.2", | ||
| "zod": "^3.23.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/express": "^4.17.21", | ||
| "@types/node": "^20.11.0", | ||
@@ -38,0 +42,0 @@ "typescript": "^5.4.0" |
+17
-0
@@ -43,2 +43,19 @@ # Wealthville MCP Server | ||
| ## Hosted / remote (HTTP) | ||
| For platforms that need an HTTPS endpoint (e.g. Smithery's "MCP Server URL"), the | ||
| same server also runs over **Streamable HTTP** at `POST /mcp` — stateless, so it | ||
| scales horizontally. Build the image and host it anywhere: | ||
| ```bash | ||
| docker build -f Dockerfile.http -t wealthville-mcp-http . | ||
| docker run -p 8080:8080 wealthville-mcp-http | ||
| # health: curl -s localhost:8080/health | ||
| # then expose it over HTTPS and use https://<your-host>/mcp | ||
| ``` | ||
| Or without Docker: `npm run build && npm run start:http` (listens on `$PORT`, default 8080). | ||
| Per-request config is accepted via `?config=<base64 JSON>`, `?wealthvilleApiKey=…`, or an | ||
| `x-api-key` header; otherwise the `WEALTHVILLE_*` env vars below apply. | ||
| ## Configuration (optional) | ||
@@ -45,0 +62,0 @@ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
12799
57.29%5
66.67%156
113.7%73
30.36%3
50%3
50%5
25%2
100%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed