0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCPでAlpaca/OANDAをClaude APIのツールにする実装詳解

0
Posted at

Hermes Agent では Alpaca / OANDA との通信を MCP(Model Context Protocol)Server として独立させています。Claude API の Tool Use とは少し異なるアーキテクチャで、なぜこうしたのか、どう実装したかを解説します。


MCP とは

MCP(Model Context Protocol)は Anthropic が提唱するオープンな標準で、LLM アプリケーションと外部ツール・データソースを疎結合でつなぐプロトコルです。

Claude API ←→ MCP Client ←→ MCP Server ←→ 外部サービス
                                             (Alpaca, OANDA, etc.)

@modelcontextprotocol/sdk で Client / Server の両方を TypeScript で実装できます。


なぜ Tool Use ではなく MCP にしたのか

Claude API には Tool Use 機能があり、関数定義を渡してモデルに呼ばせることができます。当初はこれを使っていましたが、MCP に移行した理由があります。

Claude Tool Use MCP Server
通信 API コール内でインライン 独立プロセス(stdio)
ライフサイクル リクエスト単位 常駐(WebSocket接続を維持)
WebSocket接続 都度接続 起動時に1回接続して維持
再利用性 Claude 専用 どのクライアントからでも使える

Alpaca の WebSocket 接続(リアルタイム価格キャッシュ)は常駐が必要なため、MCP Server として独立させました。


実装構成

src/mcp/
├── index.ts              # MCP Server エントリーポイント
├── providers/
│   ├── alpaca.ts         # Alpaca REST + WebSocket
│   └── oanda.ts          # OANDA REST + SSE streaming
├── tools/
│   ├── price-tools.ts    # get_realtime_price, get_historical_bars
│   ├── order-tools.ts    # place_order
│   ├── account-tools.ts  # get_account_info
│   └── calendar-tools.ts # get_economic_calendar
└── ws-manager.ts         # シンボルルーティング

MCP Server の実装

// src/mcp/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new Server(
  { name: "hermes-mcp", version: "1.0.0" },
  { capabilities: { tools: {} } },
);

// ツール一覧を返す
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    ...priceToolDefs,
    ...orderToolDefs,
    ...accountToolDefs,
    ...calendarToolDefs,
  ],
}));

// ツール呼び出しを処理
server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const { name, arguments: args } = req.params;

  if (name.startsWith("get_") && name.includes("price") || name.includes("bars")) {
    return handlePriceTool(name, args);
  }
  if (name === "place_order") {
    return handleOrderTool(name, args);
  }
  if (name === "get_account_info") {
    return handleAccountTool(name);
  }
  if (name === "get_economic_calendar") {
    return handleCalendarTool(name);
  }
  throw new Error(`Unknown tool: ${name}`);
});

// stdio で通信(サブプロセスとして起動)
const transport = new StdioServerTransport();
await server.connect(transport);

MCP Client(エージェント側)の実装

// src/agent/hermes.ts(抜粋)
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

async connect() {
  // MCP Server をサブプロセスとして起動
  const transport = new StdioClientTransport({
    command: "node",
    args: ["--import", "tsx/esm", this.cfg.mcpServerPath],
    env: { ...process.env },
  });

  this.mcp = new Client(
    { name: "hermes-agent", version: "0.11.0" },
    { capabilities: {} },
  );
  await this.mcp.connect(transport);

  // 利用可能なツール一覧を取得
  const { tools } = await this.mcp.listTools();
  this.mcpTools = tools.map(t => ({
    name: t.name,
    description: t.description ?? "",
    input_schema: t.inputSchema,
  }));
}

エージェントが起動するとき、MCP Server が サブプロセス として立ち上がります。親子プロセスは stdio で JSON-RPC 通信します。


ツールの実装例:place_order

// src/mcp/tools/order-tools.ts(抜粋)
export async function handleOrderTool(
  name: string,
  args: Record<string, unknown>,
): Promise<ToolResponse> {
  const { symbol, direction, quantity, stop_loss, take_profit } = args;
  const mode = process.env.TRADING_MODE ?? "analyze";

  // ドライランモード
  if (mode === "analyze") {
    return ok({ dry_run: true, status: "simulated", message: "Dry run — no order sent" });
  }

  // シンボルの種類でブローカーを振り分け
  if ((symbol as string).includes("_")) {
    // FX → OANDA
    return placeOandaOrder(symbol, direction, quantity, stop_loss, take_profit);
  } else {
    // 株・暗号資産 → Alpaca
    return placeAlpacaOrder(symbol, direction, quantity, stop_loss, take_profit);
  }
}

symbol の形式(_ 含む → FX、/ 含む → 暗号資産、それ以外 → 株)でブローカーを自動判定しています。


WebSocket 接続の維持

MCP Server が常駐することで、Alpaca の WebSocket 接続を起動時に1回だけ張って維持できます。

// src/mcp/ws-manager.ts(概略)
export class WsManager {
  private readonly alpaca: AlpacaProvider;

  constructor() {
    this.alpaca = new AlpacaProvider(apiKey, apiSecret, isPaper);
    // 株式・暗号資産の WebSocket を起動時に接続
    this.alpaca.connect(stockSymbols);
    this.alpaca.connectCrypto(cryptoSymbols);
  }

  // get_realtime_price はキャッシュから返す(接続オーバーヘッドなし)
  getPrice(symbol: string): PriceTick | null {
    return this.alpaca.getCached(symbol);
  }
}

毎回 REST API を叩くより WebSocket キャッシュから返す方が圧倒的に高速です。


ツールのタイムアウト制御

MCP ツール呼び出しに 30 秒のタイムアウトを設けています。Alpaca や Finnhub のレスポンスが遅い場合でもサイクル全体がハングするのを防ぐためです。

private async callToolSafe(name: string, args: Record<string, unknown>, timeoutMs = 30_000) {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error(`MCP '${name}' timed out after ${timeoutMs / 1000}s`)), timeoutMs)
  );
  return Promise.race([this.mcp.callTool({ name, arguments: args }), timeout]);
}

まとめ

ポイント 内容
常駐プロセスとして分離 WebSocket 接続をサイクルをまたいで維持できる
stdio 通信 シンプル・デバッグしやすい
シンボル形式でブローカーを自動判定 _→OANDA、/→Alpaca crypto、それ以外→Alpaca stocks
ツールタイムアウト 外部API 遅延でサイクル全体がハングするのを防止

MCP は「LLM ↔ 外部ツール」の接続方法として標準化が進んでいます。自動売買以外のユースケースでも、常駐型のツールサーバーが必要な場合に有効です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?