From b1893d9828b08dfe580393183411489603778551 Mon Sep 17 00:00:00 2001 From: kakira Date: Sat, 2 Aug 2025 15:18:57 +0900 Subject: [PATCH 01/39] =?UTF-8?q?MCP=20Server=20=E6=A9=9F=E8=83=BD?= =?UTF-8?q?=E3=81=AE=E3=82=B3=E3=82=A2=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +- doc/mcp-server-spec.md | 765 ++++++++++++++++++ .../packetproxy/extensions/mcp/MCPServer.java | 164 ++++ .../extensions/mcp/MCPServerExtension.java | 141 ++++ .../extensions/mcp/tools/ConfigTool.java | 170 ++++ .../extensions/mcp/tools/HistoryTool.java | 122 +++ .../extensions/mcp/tools/MCPTool.java | 26 + .../mcp/tools/PacketDetailTool.java | 170 ++++ .../extensions/mcp/tools/ToolRegistry.java | 55 ++ .../core/packetproxy/model/Extensions.java | 2 + 10 files changed, 1617 insertions(+), 2 deletions(-) create mode 100644 doc/mcp-server-spec.md create mode 100644 src/main/java/core/packetproxy/extensions/mcp/MCPServer.java create mode 100644 src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/MCPTool.java create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java diff --git a/build.gradle b/build.gradle index f92ab856..39a32e98 100644 --- a/build.gradle +++ b/build.gradle @@ -52,8 +52,8 @@ dependencies { implementation 'com.github.mobius-software-ltd:mqtt-parser:parser-1.0.3' implementation 'net.arnx:jsonic:1.3.0' implementation 'org.json:json:20180813' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.10.0' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.19.2' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.19.2' implementation 'org.msgpack:jackson-dataformat-msgpack:0.8.18' implementation 'org.bouncycastle:bcpkix-jdk15on:1.64' implementation 'commons-net:commons-net:3.6' diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md new file mode 100644 index 00000000..dd74370d --- /dev/null +++ b/doc/mcp-server-spec.md @@ -0,0 +1,765 @@ +# PacketProxy MCP サーバー仕様書 + +## 概要 + +PacketProxy MCP サーバーは、Model Context Protocol (MCP) を使用してPacketProxyの機能を外部から操作するためのインターフェースを提供します。AIエージェントやその他のクライアントがPacketProxyのパケット履歴取得、設定変更、パケット再送などの操作を実行できます。 + +## 基本仕様 + +- **プロトコル**: MCP (Model Context Protocol) - JSON-RPC over stdin/stdout +- **補完API**: HTTP REST API (localhost:32350) +- **認証**: アクセストークン方式 +- **実装**: PacketProxy Extension として実装 + +## アーキテクチャ + +``` +┌─────────────────┐ JSON-RPC ┌─────────────────┐ +│ MCP Client │ ←─────────────→ │ MCP Extension │ +│ (Claude, etc) │ stdin/stdout │ │ +└─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ PacketProxy │ + │ Core APIs │ + └─────────────────┘ +``` + +## MCPツール一覧 + +### 1. `get_history` - パケット履歴取得 + +PacketProxyのパケット履歴を検索・取得します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_history", + "arguments": { + "limit": 100, + "offset": 0, + "filter": "method == GET && url =~ /api/", + "columns": ["id", "method", "url", "status", "length", "time"], + "sort_by": "time", + "sort_order": "desc" + } + }, + "id": 1 +} +``` + +**パラメータ:** +- `limit` (number, optional): 取得件数 (デフォルト: 100) +- `offset` (number, optional): オフセット (デフォルト: 0) +- `filter` (string, optional): PacketProxy Filter構文による絞り込み +- `columns` (array, optional): 取得するカラム +- `sort_by` (string, optional): ソート対象カラム (デフォルト: time) +- `sort_order` (string, optional): "asc" | "desc" (デフォルト: desc) + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "packets": [ + { + "id": 123, + "method": "GET", + "url": "/api/users", + "status": 200, + "length": 1024, + "time": "2025-01-15T10:30:00Z", + "server_name": "api.example.com", + "client_ip": "192.168.1.100" + } + ], + "total_count": 1500, + "has_more": true + }, + "id": 1 +} +``` + +### 2. `get_packet_detail` - パケット詳細取得 + +特定のパケットの詳細情報を取得します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_packet_detail", + "arguments": { + "packet_id": 123, + "include_body": true + } + }, + "id": 2 +} +``` + +**パラメータ:** +- `packet_id` (number, required): パケットID +- `include_body` (boolean, optional): リクエスト/レスポンスボディを含める (デフォルト: false) + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "id": 123, + "method": "GET", + "url": "/api/users", + "status": 200, + "headers": { + "request": [ + {"name": "Host", "value": "api.example.com"}, + {"name": "User-Agent", "value": "Mozilla/5.0..."} + ], + "response": [ + {"name": "Content-Type", "value": "application/json"}, + {"name": "Content-Length", "value": "1024"} + ] + }, + "body": { + "request": "", + "response": "{\"users\": [...]}" + }, + "timing": { + "timestamp": "2025-01-15T10:30:00Z", + "duration_ms": 245 + } + }, + "id": 2 +} +``` + +### 3. `get_configs` - 設定情報取得 + +PacketProxyの設定情報を取得します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_configs", + "arguments": { + "categories": ["listenPorts", "servers"] + } + }, + "id": 3 +} +``` + +**パラメータ:** +- `categories` (array, optional): 取得するカテゴリ (空の場合は全て) +- `listenPorts`: リッスンポート設定 +- `servers`: サーバー設定 +- `modifications`: 改変設定 +- `sslPassThroughs`: SSL パススルー設定 + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "listenPorts": [ + { + "id": 1, + "port": 8080, + "protocol": "HTTP", + "serverId": 1, + "enabled": true + } + ], + "servers": [ + { + "id": 1, + "host": "target.com", + "port": 443, + "protocol": "HTTPS", + "enabled": true + } + ], + "modifications": [], + "sslPassThroughs": [] + }, + "id": 3 +} +``` + +### 4. `update_config` - 設定変更 + +PacketProxyの設定を変更します。PacketProxyHub互換の形式を使用します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "update_config", + "arguments": { + "config_json": { + "listenPorts": [ + {"id": 1, "port": 8080, "protocol": "HTTP", "serverId": 1} + ], + "servers": [ + {"id": 1, "host": "target.com", "port": 443, "protocol": "HTTPS"} + ], + "modifications": [ + {"id": 1, "name": "Add Header", "pattern": ".*", "replacement": "X-Test: 1"} + ], + "sslPassThroughs": [ + {"id": 1, "host": "secure.com", "port": 443} + ] + }, + "backup": true + } + }, + "id": 4 +} +``` + +**パラメータ:** +- `config_json` (object, required): PacketProxyHub互換の設定JSON +- `backup` (boolean, optional): 既存設定をバックアップ (デフォルト: true) + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "success": true, + "backup_created": true, + "backup_info": { + "backup_id": "backup_20250115_103000", + "backup_path": "/path/to/backups/config_backup_20250115_103000.json", + "timestamp": "2025-01-15T10:30:00Z" + }, + "config_updated": true + }, + "id": 4 +} +``` + +### 5. `resend_packet` - パケット再送 + +パケットを再送します。パケット改変や連続送信に対応しています。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "resend_packet", + "arguments": { + "packet_id": 123, + "count": 20, + "interval_ms": 100, + "modifications": [ + { + "target": "request", + "type": "regex_replace", + "pattern": "sessionId=\\w+", + "replacement": "sessionId=modified_{{index}}" + }, + { + "type": "header_add", + "name": "X-Test-Counter", + "value": "{{timestamp}}" + } + ], + "async": false + } + }, + "id": 5 +} +``` + +**パラメータ:** +- `packet_id` (number, required): 再送するパケットID +- `count` (number, optional): 送信回数 (デフォルト: 1) +- `interval_ms` (number, optional): 送信間隔(ms) (デフォルト: 0) +- `modifications` (array, optional): パケット改変設定 +- `async` (boolean, optional): 非同期実行 (デフォルト: false) + +**改変設定:** +- `target`: "request" | "response" | "both" +- `type`: "regex_replace" | "header_add" | "header_modify" +- `pattern`: 正規表現パターン (regex_replaceの場合) +- `replacement` / `value`: 置換文字列 +- `name`: ヘッダー名 (header_add/header_modifyの場合) + +**置換変数:** +- `{{index}}`: 送信順序 (1, 2, 3...) +- `{{timestamp}}`: Unix timestamp +- `{{random}}`: ランダム文字列(8文字) +- `{{uuid}}`: UUID v4 +- `{{datetime}}`: ISO 8601形式日時 + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "success": true, + "sent_count": 20, + "failed_count": 0, + "packet_ids": [124, 125, 126], + "execution_time_ms": 2100 + }, + "id": 5 +} +``` + +### 6. `get_logs` - ログ取得 + +PacketProxyのログを取得します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_logs", + "arguments": { + "level": "info", + "limit": 100, + "since": "2025-01-15T00:00:00Z", + "filter": "error|exception" + } + }, + "id": 6 +} +``` + +**パラメータ:** +- `level` (string, optional): ログレベル "debug" | "info" | "warn" | "error" +- `limit` (number, optional): 取得件数 (デフォルト: 100) +- `since` (string, optional): 開始時刻 (ISO 8601形式) +- `filter` (string, optional): 正規表現フィルタ + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "logs": [ + { + "timestamp": "2025-01-15T10:30:00Z", + "level": "info", + "message": "PacketProxy started successfully", + "thread": "main", + "class": "packetproxy.PacketProxy" + } + ], + "total_count": 1500, + "has_more": true + }, + "id": 6 +} +``` + +### 7. `get_filters` - フィルタ定義取得 + +保存済みフィルタと利用可能なカラム情報を取得します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_filters", + "arguments": { + "include_examples": true + } + }, + "id": 7 +} +``` + +**パラメータ:** +- `include_examples` (boolean, optional): サンプルフィルタも含める (デフォルト: false) + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "saved_filters": [ + { + "id": 1, + "name": "API Requests", + "filter": "url =~ /api/ && method == GET" + } + ], + "available_columns": [ + { + "name": "id", + "type": "integer", + "description": "パケットID" + }, + { + "name": "request", + "type": "string", + "description": "リクエスト内容" + } + ], + "operators": [ + { + "operator": "==", + "description": "等しい", + "example": "method == GET" + } + ], + "example_filters": [ + { + "name": "HTTP Errors", + "filter": "status >= 400 && status <= 599" + } + ] + }, + "id": 7 +} +``` + +### 8. `validate_filter` - フィルタ構文検証 + +フィルタ構文の妥当性を検証します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "validate_filter", + "arguments": { + "filter": "method == GET && url =~ /api/", + "explain": true + } + }, + "id": 8 +} +``` + +**パラメータ:** +- `filter` (string, required): 検証するフィルタ構文 +- `explain` (boolean, optional): 詳細説明を含める (デフォルト: false) + +**成功レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "valid": true, + "explanation": { + "description": "HTTPメソッドがGETかつURLに'/api/'が含まれるパケットをフィルタ", + "parsed_conditions": [ + { + "column": "method", + "operator": "==", + "value": "GET", + "description": "HTTPメソッドがGETと等しい" + } + ], + "logical_operator": "AND" + }, + "estimated_performance": "fast", + "recommendations": [ + "正規表現パフォーマンスが良好です" + ] + }, + "id": 8 +} +``` + +**エラーレスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "valid": false, + "errors": [ + { + "type": "syntax_error", + "message": "Invalid operator '===' at position 15", + "position": 15, + "context": "method === GET" + } + ], + "suggestions": [ + "Use single '=' instead of '==='" + ] + }, + "id": 8 +} +``` + +### 9. `get_config_backups` - 設定バックアップ一覧取得 + +作成された設定バックアップの一覧を取得します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_config_backups", + "arguments": { + "limit": 10 + } + }, + "id": 9 +} +``` + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "backups": [ + { + "backup_id": "backup_20250115_103000", + "timestamp": "2025-01-15T10:30:00Z", + "size_bytes": 2048, + "description": "Backup before MCP config update" + } + ], + "total_count": 5 + }, + "id": 9 +} +``` + +### 10. `restore_config_backup` - 設定バックアップ復元 + +指定したバックアップから設定を復元します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "restore_config_backup", + "arguments": { + "backup_id": "backup_20250115_103000" + } + }, + "id": 10 +} +``` + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "success": true, + "restored_from": "backup_20250115_103000", + "backup_created": "backup_20250115_104500" + }, + "id": 10 +} +``` + +## フィルタ構文仕様 + +PacketProxyのFilterTextParserに準拠した構文を使用します。 + +### 利用可能カラム + +| カラム名 | 型 | 説明 | +|---------------|----------|-----------------| +| `id` | integer | パケットID | +| `request` | string | リクエスト内容 | +| `response` | string | レスポンス内容 | +| `length` | integer | パケットサイズ | +| `client_ip` | string | クライアントIP | +| `client_port` | integer | クライアントポート | +| `server_ip` | string | サーバーIP | +| `server_port` | integer | サーバーポート | +| `time` | datetime | タイムスタンプ | +| `resend` | boolean | 再送フラグ | +| `modified` | boolean | 改変フラグ | +| `type` | string | プロトコルタイプ | +| `encode` | string | エンコーダ種別 | +| `alpn` | string | ALPN情報 | +| `group` | string | グループ名 | +| `full_text` | string | 全文検索 (大文字小文字区別) | +| `full_text_i` | string | 全文検索 (大文字小文字無視) | + +### 演算子 + +| 演算子 | 説明 | 例 | +|--------|----------|-------------------------------------| +| `==` | 等しい | `method == GET` | +| `!=` | 等しくない | `status != 200` | +| `>=` | 以上 | `length >= 1000` | +| `<=` | 以下 | `status <= 299` | +| `=~` | 正規表現マッチ | `url =~ /api/v[0-9]+/` | +| `!~` | 正規表現非マッチ | `url !~ /static/` | +| `&&` | AND演算 | `method == POST && status >= 400` | +| `\|\|` | OR演算 | `method == GET \|\| method == POST` | + +### フィルタ例 + +``` +# HTTP エラー +status >= 400 && status <= 599 + +# 大きなリクエスト +length > 10000 + +# API コール +url =~ /api/ && (method == GET || method == POST) + +# 認証関連 +full_text_i =~ authorization + +# WebSocket トラフィック +type == WebSocket + +# 複合条件 +method == POST && url =~ /login && status == 401 +``` + +## HTTP REST API (補完) + +MCP以外の方法でもアクセス可能なHTTP REST APIを提供します。 + +### エンドポイント + +``` +GET /mcp/tools # ツール一覧 +GET /mcp/history?filter=...&limit=100 # パケット履歴 +GET /mcp/packet/{id} # パケット詳細 +GET /mcp/configs # 設定一覧 +PUT /mcp/configs # 設定更新 +POST /mcp/resend/{packet_id} # パケット再送 +GET /mcp/logs?level=info # ログ取得 +GET /mcp/filters # フィルタ一覧 +POST /mcp/filters/validate # フィルタ検証 +GET /mcp/backups # バックアップ一覧 +POST /mcp/backups/{backup_id}/restore # バックアップ復元 +``` + +### 認証 + +HTTP APIはアクセストークンによる認証を使用します。 + +```http +Authorization: Bearer +``` + +## エラーハンドリング + +### MCP標準エラー + +```json +{ + "jsonrpc": "2.0", + "error": { + "code": -32602, + "message": "Invalid params", + "data": { + "details": "packet_id is required" + } + }, + "id": 1 +} +``` + +### カスタムエラーコード + +| コード | 説明 | +|--------|------------------------------| +| -32001 | PacketProxy connection error | +| -32002 | Invalid filter syntax | +| -32003 | Packet not found | +| -32004 | Configuration error | +| -32005 | Permission denied | + +## セキュリティ考慮事項 + +1. **アクセス制御**: 設定変更操作は適切な権限を要求 +2. **入力検証**: すべての入力パラメータを検証 +3. **ログ記録**: 重要な操作はログに記録 +4. **レート制限**: パケット再送などの高負荷操作に制限 +5. **設定バックアップ**: 重要な設定変更前に自動バックアップ + +## パフォーマンス考慮事項 + +1. **フィルタ最適化**: 複雑なフィルタは性能警告を表示 +2. **ページング**: 大量データは適切にページング +3. **キャッシュ**: 頻繁にアクセスされるデータはキャッシュ +4. **非同期処理**: 時間のかかる操作は非同期実行をサポート + +## 実装詳細 + +### ディレクトリ構成 + +``` +src/main/java/core/packetproxy/extensions/mcp/ +├── MCPServerExtension.java # Extension基底クラス継承 +├── MCPServer.java # MCP JSONRPCサーバー実装 +├── tools/ +│ ├── HistoryTool.java # History情報取得 +│ ├── SettingTool.java # 設定情報取得・変更 +│ ├── LogTool.java # ログ情報取得 +│ ├── ResendTool.java # パケット再送 +│ └── FilterTool.java # フィルタ関連操作 +├── MCPToolRegistry.java # ツール登録・管理 +└── backup/ + └── ConfigBackupManager.java # 設定バックアップ管理 +``` + +### 統合ポイント + +- **Extension管理**: GUIOptionExtensionsで有効/無効切り替え +- **データアクセス**: 既存のPackets, Configs, Filters等のAPIを活用 +- **パケット操作**: ResendControllerを使用した再送機能 +- **設定管理**: ConfigIOを使用したPacketProxyHub互換性 + +## バージョン履歴 + +| バージョン | 日付 | 変更内容 | +|-------|------------|------| +| 1.0.0 | 2025-01-15 | 初版作成 | + diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java new file mode 100644 index 00000000..90295ee5 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java @@ -0,0 +1,164 @@ +package packetproxy.extensions.mcp; + +import static packetproxy.util.Logging.log; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.util.function.Consumer; +import packetproxy.extensions.mcp.tools.ToolRegistry; + +public class MCPServer { + + private final Gson gson; + private final ToolRegistry toolRegistry; + private final Consumer logger; + private boolean running = false; + private BufferedReader reader; + private PrintWriter writer; + + public MCPServer(Consumer logger) { + this.gson = new GsonBuilder().setPrettyPrinting().create(); + this.toolRegistry = new ToolRegistry(); + this.logger = logger; + this.reader = new BufferedReader(new InputStreamReader(System.in)); + this.writer = new PrintWriter(System.out, true); + } + + public void run() throws IOException { + running = true; + logger.accept("MCP Server listening on stdin/stdout"); + + while (running) { + try { + String line = reader.readLine(); + if (line == null) { + break; // EOF + } + + line = line.trim(); + if (line.isEmpty()) { + continue; + } + + logger.accept("Received: " + line); + processRequest(line); + + } catch (Exception e) { + logger.accept("Error processing request: " + e.getMessage()); + log("MCP Server error: " + e.getMessage()); + e.printStackTrace(); + } + } + } + + public void stop() { + running = false; + try { + if (reader != null) { + reader.close(); + } + if (writer != null) { + writer.close(); + } + } catch (IOException e) { + logger.accept("Error closing streams: " + e.getMessage()); + } + } + + private void processRequest(String requestLine) { + try { + JsonObject request = JsonParser.parseString(requestLine).getAsJsonObject(); + + String method = request.get("method").getAsString(); + JsonElement id = request.get("id"); + JsonObject params = request.has("params") ? request.getAsJsonObject("params") : new JsonObject(); + + JsonObject response = new JsonObject(); + response.addProperty("jsonrpc", "2.0"); + if (id != null) { + response.add("id", id); + } + + try { + JsonObject result = handleMethod(method, params); + response.add("result", result); + } catch (Exception e) { + JsonObject error = new JsonObject(); + error.addProperty("code", -32603); + error.addProperty("message", "Internal error: " + e.getMessage()); + response.add("error", error); + logger.accept("Method error: " + e.getMessage()); + } + + String responseString = gson.toJson(response); + writer.println(responseString); + logger.accept("Sent: " + responseString); + + } catch (Exception e) { + // Invalid JSON request + JsonObject errorResponse = new JsonObject(); + errorResponse.addProperty("jsonrpc", "2.0"); + errorResponse.add("id", null); + + JsonObject error = new JsonObject(); + error.addProperty("code", -32700); + error.addProperty("message", "Parse error"); + errorResponse.add("error", error); + + writer.println(gson.toJson(errorResponse)); + logger.accept("Parse error: " + e.getMessage()); + } + } + + private JsonObject handleMethod(String method, JsonObject params) throws Exception { + switch (method) { + case "initialize" : + return handleInitialize(params); + case "tools/list" : + return handleToolsList(); + case "tools/call" : + return handleToolsCall(params); + default : + throw new Exception("Unknown method: " + method); + } + } + + private JsonObject handleInitialize(JsonObject params) { + JsonObject result = new JsonObject(); + + JsonObject capabilities = new JsonObject(); + JsonObject tools = new JsonObject(); + tools.addProperty("listChanged", true); + capabilities.add("tools", tools); + + result.add("capabilities", capabilities); + result.addProperty("serverInfo", "PacketProxy MCP Server v1.0"); + + logger.accept("Client initialized"); + return result; + } + + private JsonObject handleToolsList() { + JsonObject result = new JsonObject(); + result.add("tools", toolRegistry.getToolsList()); + return result; + } + + private JsonObject handleToolsCall(JsonObject params) throws Exception { + if (!params.has("name")) { + throw new Exception("Tool name is required"); + } + + String toolName = params.get("name").getAsString(); + JsonObject arguments = params.has("arguments") ? params.getAsJsonObject("arguments") : new JsonObject(); + + return toolRegistry.callTool(toolName, arguments); + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java new file mode 100644 index 00000000..d65e04c4 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java @@ -0,0 +1,141 @@ +package packetproxy.extensions.mcp; + +import static packetproxy.util.Logging.log; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import packetproxy.model.Extension; + +public class MCPServerExtension extends Extension { + + private MCPServer server; + private JTextArea logArea; + private JButton startButton; + private JButton stopButton; + private boolean isRunning = false; + + public MCPServerExtension() { + super(); + this.setName("MCP Server"); + } + + public MCPServerExtension(String name, String path) throws Exception { + super(name, path); + this.setName("MCP Server"); + } + + @Override + public JComponent createPanel() throws Exception { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + // Status panel + JPanel statusPanel = new JPanel(); + statusPanel.setLayout(new BoxLayout(statusPanel, BoxLayout.X_AXIS)); + statusPanel.add(new JLabel("MCP Server Status: ")); + + startButton = new JButton("Start Server"); + stopButton = new JButton("Stop Server"); + stopButton.setEnabled(false); + + startButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + startServer(); + } + }); + + stopButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + stopServer(); + } + }); + + statusPanel.add(startButton); + statusPanel.add(stopButton); + + // Log area + logArea = new JTextArea(20, 80); + logArea.setEditable(false); + JScrollPane scrollPane = new JScrollPane(logArea); + + panel.add(statusPanel); + panel.add(new JLabel("Server Logs:")); + panel.add(scrollPane); + + return panel; + } + + @Override + public JMenuItem historyClickHandler() { + return null; // MCP Serverは右クリックメニューに追加しない + } + + private void startServer() { + if (isRunning) { + return; + } + + try { + server = new MCPServer(this::addLog); + Thread serverThread = new Thread(() -> { + try { + server.run(); + } catch (Exception e) { + addLog("Server error: " + e.getMessage()); + e.printStackTrace(); + } + }); + serverThread.setDaemon(true); + serverThread.start(); + + isRunning = true; + startButton.setEnabled(false); + stopButton.setEnabled(true); + addLog("MCP Server started"); + log("MCP Server started"); + + } catch (Exception e) { + addLog("Failed to start server: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void stopServer() { + if (!isRunning || server == null) { + return; + } + + try { + server.stop(); + server = null; + isRunning = false; + startButton.setEnabled(true); + stopButton.setEnabled(false); + addLog("MCP Server stopped"); + log("MCP Server stopped"); + + } catch (Exception e) { + addLog("Failed to stop server: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void addLog(String message) { + if (logArea != null) { + javax.swing.SwingUtilities.invokeLater(() -> { + logArea.append("[" + new java.util.Date() + "] " + message + "\n"); + logArea.setCaretPosition(logArea.getDocument().getLength()); + }); + } + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java new file mode 100644 index 00000000..5f241af3 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java @@ -0,0 +1,170 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.List; +import packetproxy.model.ListenPort; +import packetproxy.model.ListenPorts; +import packetproxy.model.Modification; +import packetproxy.model.Modifications; +import packetproxy.model.SSLPassThrough; +import packetproxy.model.SSLPassThroughs; +import packetproxy.model.Server; +import packetproxy.model.Servers; + +public class ConfigTool implements MCPTool { + + @Override + public String getName() { + return "get_configs"; + } + + @Override + public String getDescription() { + return "Get PacketProxy configuration settings"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject categoriesProp = new JsonObject(); + categoriesProp.addProperty("type", "array"); + categoriesProp.addProperty("description", "Categories to retrieve (empty for all)"); + + JsonObject itemsProp = new JsonObject(); + itemsProp.addProperty("type", "string"); + JsonArray enumValues = new JsonArray(); + enumValues.add("listenPorts"); + enumValues.add("servers"); + enumValues.add("modifications"); + enumValues.add("sslPassThroughs"); + itemsProp.add("enum", enumValues); + categoriesProp.add("items", itemsProp); + + schema.add("categories", categoriesProp); + + return schema; + } + + @Override + public JsonObject call(JsonObject arguments) throws Exception { + log("ConfigTool called with arguments: " + arguments.toString()); + + JsonArray categories = null; + if (arguments.has("categories")) { + categories = arguments.getAsJsonArray("categories"); + } + + JsonObject result = new JsonObject(); + + try { + if (shouldIncludeCategory(categories, "listenPorts")) { + result.add("listenPorts", getListenPorts()); + } + + if (shouldIncludeCategory(categories, "servers")) { + result.add("servers", getServers()); + } + + if (shouldIncludeCategory(categories, "modifications")) { + result.add("modifications", getModifications()); + } + + if (shouldIncludeCategory(categories, "sslPassThroughs")) { + result.add("sslPassThroughs", getSSLPassThroughs()); + } + + log("ConfigTool returning configuration"); + return result; + + } catch (Exception e) { + log("ConfigTool error: " + e.getMessage()); + throw new Exception("Failed to get configuration: " + e.getMessage()); + } + } + + private boolean shouldIncludeCategory(JsonArray categories, String category) { + if (categories == null || categories.size() == 0) { + return true; // Include all if not specified + } + + for (int i = 0; i < categories.size(); i++) { + if (categories.get(i).getAsString().equals(category)) { + return true; + } + } + return false; + } + + private JsonArray getListenPorts() throws Exception { + JsonArray portsArray = new JsonArray(); + List ports = ListenPorts.getInstance().queryAll(); + + for (ListenPort port : ports) { + JsonObject portJson = new JsonObject(); + portJson.addProperty("id", port.getId()); + portJson.addProperty("port", port.getPort()); + portJson.addProperty("protocol", port.getProtocol().toString()); + portJson.addProperty("type", port.getType().toString()); + portJson.addProperty("serverId", port.getServerId()); + portJson.addProperty("enabled", port.isEnabled()); + portsArray.add(portJson); + } + + return portsArray; + } + + private JsonArray getServers() throws Exception { + JsonArray serversArray = new JsonArray(); + List servers = Servers.getInstance().queryAll(); + + for (Server server : servers) { + JsonObject serverJson = new JsonObject(); + serverJson.addProperty("id", server.getId()); + serverJson.addProperty("ip", server.getIp()); + serverJson.addProperty("port", server.getPort()); + serverJson.addProperty("encoder", server.getEncoder()); + serverJson.addProperty("use_ssl", server.getUseSSL()); + serverJson.addProperty("comment", server.getComment()); + serversArray.add(serverJson); + } + + return serversArray; + } + + private JsonArray getModifications() throws Exception { + JsonArray modificationsArray = new JsonArray(); + List modifications = Modifications.getInstance().queryAll(); + + for (Modification mod : modifications) { + JsonObject modJson = new JsonObject(); + modJson.addProperty("id", mod.getId()); + modJson.addProperty("direction", mod.getDirection().toString()); + modJson.addProperty("method", mod.getMethod().toString()); + modJson.addProperty("pattern", mod.getPattern()); + modJson.addProperty("replaced", mod.getReplaced()); + modJson.addProperty("serverId", mod.getServerId()); + modificationsArray.add(modJson); + } + + return modificationsArray; + } + + private JsonArray getSSLPassThroughs() throws Exception { + JsonArray passThroughsArray = new JsonArray(); + List passThroughs = SSLPassThroughs.getInstance().queryAll(); + + for (SSLPassThrough passThrough : passThroughs) { + JsonObject passThroughJson = new JsonObject(); + passThroughJson.addProperty("id", passThrough.getId()); + passThroughJson.addProperty("server_name", passThrough.getServerName()); + passThroughJson.addProperty("listen_port", passThrough.getListenPort()); + passThroughsArray.add(passThroughJson); + } + + return passThroughsArray; + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java new file mode 100644 index 00000000..f2850a0d --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java @@ -0,0 +1,122 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.text.SimpleDateFormat; +import java.util.List; +import packetproxy.model.Packet; +import packetproxy.model.Packets; + +public class HistoryTool implements MCPTool { + + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + @Override + public String getName() { + return "get_history"; + } + + @Override + public String getDescription() { + return "Get packet history from PacketProxy"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject limitProp = new JsonObject(); + limitProp.addProperty("type", "integer"); + limitProp.addProperty("description", "Maximum number of packets to return"); + limitProp.addProperty("default", 100); + schema.add("limit", limitProp); + + JsonObject offsetProp = new JsonObject(); + offsetProp.addProperty("type", "integer"); + offsetProp.addProperty("description", "Number of packets to skip"); + offsetProp.addProperty("default", 0); + schema.add("offset", offsetProp); + + return schema; + } + + @Override + public JsonObject call(JsonObject arguments) throws Exception { + log("HistoryTool called with arguments: " + arguments.toString()); + + int limit = arguments.has("limit") ? arguments.get("limit").getAsInt() : 100; + int offset = arguments.has("offset") ? arguments.get("offset").getAsInt() : 0; + + // Validate parameters + if (limit < 1 || limit > 1000) { + throw new Exception("Limit must be between 1 and 1000"); + } + if (offset < 0) { + throw new Exception("Offset must be non-negative"); + } + + try { + Packets packets = Packets.getInstance(); + List allPackets = packets.queryAll(); + + JsonObject result = new JsonObject(); + JsonArray packetsArray = new JsonArray(); + + int totalCount = allPackets.size(); + int startIndex = Math.min(offset, totalCount); + int endIndex = Math.min(startIndex + limit, totalCount); + + for (int i = startIndex; i < endIndex; i++) { + Packet packet = allPackets.get(i); + JsonObject packetJson = convertPacketToJson(packet); + packetsArray.add(packetJson); + } + + result.add("packets", packetsArray); + result.addProperty("total_count", totalCount); + result.addProperty("has_more", endIndex < totalCount); + + log("HistoryTool returning " + packetsArray.size() + " packets"); + return result; + + } catch (Exception e) { + log("HistoryTool error: " + e.getMessage()); + throw new Exception("Failed to get packet history: " + e.getMessage()); + } + } + + private JsonObject convertPacketToJson(Packet packet) { + JsonObject packetJson = new JsonObject(); + + packetJson.addProperty("id", packet.getId()); + packetJson.addProperty("length", packet.getDecodedData().length); + packetJson.addProperty("client_ip", packet.getClientIP()); + packetJson.addProperty("client_port", packet.getClientPort()); + packetJson.addProperty("server_ip", packet.getServerIP()); + packetJson.addProperty("server_port", packet.getServerPort()); + packetJson.addProperty("time", dateFormat.format(packet.getDate())); + packetJson.addProperty("resend", packet.getResend()); + packetJson.addProperty("modified", packet.getModified()); + packetJson.addProperty("type", packet.getContentType()); + packetJson.addProperty("encode", packet.getEncoder()); + + // HTTPの場合、methodとurlを抽出 + try { + String request = new String(packet.getDecodedData(), "UTF-8"); + String[] lines = request.split("\n"); + if (lines.length > 0) { + String[] requestLine = lines[0].split(" "); + if (requestLine.length >= 2) { + packetJson.addProperty("method", requestLine[0]); + packetJson.addProperty("url", requestLine[1]); + } + } + } catch (Exception e) { + // HTTP以外のパケットの場合は無視 + } + + return packetJson; + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/MCPTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/MCPTool.java new file mode 100644 index 00000000..fb933f25 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/MCPTool.java @@ -0,0 +1,26 @@ +package packetproxy.extensions.mcp.tools; + +import com.google.gson.JsonObject; + +public interface MCPTool { + + /** + * ツール名を取得 + */ + String getName(); + + /** + * ツールの説明を取得 + */ + String getDescription(); + + /** + * 入力スキーマを取得 (JSON Schema properties形式) + */ + JsonObject getInputSchema(); + + /** + * ツールを実行 + */ + JsonObject call(JsonObject arguments) throws Exception; +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java new file mode 100644 index 00000000..5436e295 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java @@ -0,0 +1,170 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import packetproxy.model.Packet; +import packetproxy.model.Packets; + +public class PacketDetailTool implements MCPTool { + + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + @Override + public String getName() { + return "get_packet_detail"; + } + + @Override + public String getDescription() { + return "Get detailed information about a specific packet"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject packetIdProp = new JsonObject(); + packetIdProp.addProperty("type", "integer"); + packetIdProp.addProperty("description", "ID of the packet to retrieve"); + schema.add("packet_id", packetIdProp); + + JsonObject includeBodyProp = new JsonObject(); + includeBodyProp.addProperty("type", "boolean"); + includeBodyProp.addProperty("description", "Whether to include request/response body"); + includeBodyProp.addProperty("default", false); + schema.add("include_body", includeBodyProp); + + return schema; + } + + @Override + public JsonObject call(JsonObject arguments) throws Exception { + log("PacketDetailTool called with arguments: " + arguments.toString()); + + if (!arguments.has("packet_id")) { + throw new Exception("packet_id is required"); + } + + int packetId = arguments.get("packet_id").getAsInt(); + boolean includeBody = arguments.has("include_body") && arguments.get("include_body").getAsBoolean(); + + try { + Packets packets = Packets.getInstance(); + Packet packet = packets.query(packetId); + + if (packet == null) { + throw new Exception("Packet not found: " + packetId); + } + + JsonObject result = buildPacketDetail(packet, includeBody); + + log("PacketDetailTool returning packet " + packetId); + return result; + + } catch (Exception e) { + log("PacketDetailTool error: " + e.getMessage()); + throw new Exception("Failed to get packet detail: " + e.getMessage()); + } + } + + private JsonObject buildPacketDetail(Packet packet, boolean includeBody) throws Exception { + JsonObject result = new JsonObject(); + + // Basic packet info + result.addProperty("id", packet.getId()); + result.addProperty("length", packet.getDecodedData().length); + result.addProperty("time", dateFormat.format(packet.getDate())); + result.addProperty("resend", packet.getResend()); + result.addProperty("modified", packet.getModified()); + result.addProperty("type", packet.getContentType()); + result.addProperty("encode", packet.getEncoder()); + + // Client/Server info + JsonObject client = new JsonObject(); + client.addProperty("ip", packet.getClientIP()); + client.addProperty("port", packet.getClientPort()); + result.add("client", client); + + JsonObject server = new JsonObject(); + server.addProperty("ip", packet.getServerIP()); + server.addProperty("port", packet.getServerPort()); + result.add("server", server); + + // Parse HTTP data if possible + try { + String data = new String(packet.getDecodedData(), StandardCharsets.UTF_8); + parseHttpData(result, data, includeBody); + } catch (Exception e) { + // Not HTTP or parsing failed, include raw data + if (includeBody) { + result.addProperty("raw_data", new String(packet.getDecodedData(), StandardCharsets.UTF_8)); + } + } + + return result; + } + + private void parseHttpData(JsonObject result, String data, boolean includeBody) { + String[] parts = data.split("\r\n\r\n", 2); + if (parts.length == 0) + return; + + String headers = parts[0]; + String body = parts.length > 1 ? parts[1] : ""; + + String[] lines = headers.split("\r\n"); + if (lines.length == 0) + return; + + // Parse request/response line + String firstLine = lines[0]; + if (firstLine.startsWith("HTTP/")) { + // Response + String[] statusParts = firstLine.split(" ", 3); + if (statusParts.length >= 2) { + try { + int status = Integer.parseInt(statusParts[1]); + result.addProperty("status", status); + if (statusParts.length >= 3) { + result.addProperty("status_text", statusParts[2]); + } + } catch (NumberFormatException e) { + // Invalid status code + } + } + } else { + // Request + String[] requestParts = firstLine.split(" ", 3); + if (requestParts.length >= 2) { + result.addProperty("method", requestParts[0]); + result.addProperty("url", requestParts[1]); + if (requestParts.length >= 3) { + result.addProperty("version", requestParts[2]); + } + } + } + + // Parse headers + JsonArray headersArray = new JsonArray(); + for (int i = 1; i < lines.length; i++) { + String line = lines[i]; + int colonIndex = line.indexOf(':'); + if (colonIndex > 0) { + JsonObject header = new JsonObject(); + header.addProperty("name", line.substring(0, colonIndex).trim()); + header.addProperty("value", line.substring(colonIndex + 1).trim()); + headersArray.add(header); + } + } + result.add("headers", headersArray); + + // Include body if requested + if (includeBody && !body.isEmpty()) { + result.addProperty("body", body); + } + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java new file mode 100644 index 00000000..2b62d0f5 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java @@ -0,0 +1,55 @@ +package packetproxy.extensions.mcp.tools; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.HashMap; +import java.util.Map; + +public class ToolRegistry { + + private final Map tools; + + public ToolRegistry() { + this.tools = new HashMap<>(); + registerDefaultTools(); + } + + private void registerDefaultTools() { + // 基本的な3つのツールを登録 + registerTool(new HistoryTool()); + registerTool(new PacketDetailTool()); + registerTool(new ConfigTool()); + } + + public void registerTool(MCPTool tool) { + tools.put(tool.getName(), tool); + } + + public JsonArray getToolsList() { + JsonArray toolsArray = new JsonArray(); + + for (MCPTool tool : tools.values()) { + JsonObject toolInfo = new JsonObject(); + toolInfo.addProperty("name", tool.getName()); + toolInfo.addProperty("description", tool.getDescription()); + + JsonObject inputSchema = new JsonObject(); + inputSchema.addProperty("type", "object"); + inputSchema.add("properties", tool.getInputSchema()); + + toolInfo.add("inputSchema", inputSchema); + toolsArray.add(toolInfo); + } + + return toolsArray; + } + + public JsonObject callTool(String toolName, JsonObject arguments) throws Exception { + MCPTool tool = tools.get(toolName); + if (tool == null) { + throw new Exception("Unknown tool: " + toolName); + } + + return tool.call(arguments); + } +} diff --git a/src/main/java/core/packetproxy/model/Extensions.java b/src/main/java/core/packetproxy/model/Extensions.java index cac9fdc9..7d452926 100644 --- a/src/main/java/core/packetproxy/model/Extensions.java +++ b/src/main/java/core/packetproxy/model/Extensions.java @@ -33,6 +33,7 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; import javax.swing.JOptionPane; +import packetproxy.extensions.mcp.MCPServerExtension; import packetproxy.extensions.randomness.RandomnessExtension; import packetproxy.extensions.samplehttp.SampleEncoders; import packetproxy.model.Database.DatabaseMessage; @@ -54,6 +55,7 @@ public static Extensions getInstance() throws Exception { { + put((new MCPServerExtension()).getName(), MCPServerExtension.class); put((new RandomnessExtension()).getName(), RandomnessExtension.class); put((new SampleEncoders()).getName(), SampleEncoders.class); } From c60ed32327a24124d8e00991220515a1e06bbb10 Mon Sep 17 00:00:00 2001 From: kakira Date: Sat, 2 Aug 2025 15:29:38 +0900 Subject: [PATCH 02/39] =?UTF-8?q?MCP=20Server=20=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E7=94=A8=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92=E4=BD=9C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../packetproxy/extensions/mcp/MCPServer.java | 25 ++++++ .../extensions/mcp/MCPServerExtension.java | 78 +++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java index 90295ee5..c3e9c044 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java @@ -72,6 +72,31 @@ public void stop() { } } + public JsonObject processTestRequest(JsonObject request) throws Exception { + String method = request.get("method").getAsString(); + JsonElement id = request.get("id"); + JsonObject params = request.has("params") ? request.getAsJsonObject("params") : new JsonObject(); + + JsonObject response = new JsonObject(); + response.addProperty("jsonrpc", "2.0"); + if (id != null) { + response.add("id", id); + } + + try { + JsonObject result = handleMethod(method, params); + response.add("result", result); + } catch (Exception e) { + JsonObject error = new JsonObject(); + error.addProperty("code", -32603); + error.addProperty("message", "Internal error: " + e.getMessage()); + response.add("error", error); + throw e; + } + + return response; + } + private void processRequest(String requestLine) { try { JsonObject request = JsonParser.parseString(requestLine).getAsJsonObject(); diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java index d65e04c4..1fc72ea5 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java @@ -2,6 +2,7 @@ import static packetproxy.util.Logging.log; +import com.google.gson.JsonObject; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.BoxLayout; @@ -20,6 +21,7 @@ public class MCPServerExtension extends Extension { private JTextArea logArea; private JButton startButton; private JButton stopButton; + private JButton testButton; private boolean isRunning = false; public MCPServerExtension() { @@ -44,7 +46,9 @@ public JComponent createPanel() throws Exception { startButton = new JButton("Start Server"); stopButton = new JButton("Stop Server"); + testButton = new JButton("Test Tools"); stopButton.setEnabled(false); + testButton.setEnabled(false); startButton.addActionListener(new ActionListener() { @Override @@ -60,8 +64,16 @@ public void actionPerformed(ActionEvent e) { } }); + testButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + testTools(); + } + }); + statusPanel.add(startButton); statusPanel.add(stopButton); + statusPanel.add(testButton); // Log area logArea = new JTextArea(20, 80); @@ -101,6 +113,7 @@ private void startServer() { isRunning = true; startButton.setEnabled(false); stopButton.setEnabled(true); + testButton.setEnabled(true); addLog("MCP Server started"); log("MCP Server started"); @@ -121,6 +134,7 @@ private void stopServer() { isRunning = false; startButton.setEnabled(true); stopButton.setEnabled(false); + testButton.setEnabled(false); addLog("MCP Server stopped"); log("MCP Server stopped"); @@ -130,6 +144,70 @@ private void stopServer() { } } + private void testTools() { + if (!isRunning || server == null) { + addLog("Server is not running. Please start the server first."); + return; + } + + addLog("Starting MCP tools test..."); + + Thread testThread = new Thread(() -> { + try { + // Test 1: Tools list + addLog("Test 1: Getting tools list..."); + JsonObject toolsRequest = new JsonObject(); + toolsRequest.addProperty("jsonrpc", "2.0"); + toolsRequest.addProperty("method", "tools/list"); + toolsRequest.addProperty("id", 1); + + JsonObject toolsResult = server.processTestRequest(toolsRequest); + addLog("Tools list result: " + toolsResult.toString()); + + // Test 2: Get configs + addLog("Test 2: Getting configurations..."); + JsonObject configRequest = new JsonObject(); + configRequest.addProperty("jsonrpc", "2.0"); + configRequest.addProperty("method", "tools/call"); + + JsonObject configParams = new JsonObject(); + configParams.addProperty("name", "get_configs"); + configParams.add("arguments", new JsonObject()); + configRequest.add("params", configParams); + configRequest.addProperty("id", 2); + + JsonObject configResult = server.processTestRequest(configRequest); + addLog("Config result: " + configResult.toString()); + + // Test 3: Get history (limited) + addLog("Test 3: Getting packet history..."); + JsonObject historyRequest = new JsonObject(); + historyRequest.addProperty("jsonrpc", "2.0"); + historyRequest.addProperty("method", "tools/call"); + + JsonObject historyParams = new JsonObject(); + historyParams.addProperty("name", "get_history"); + JsonObject historyArgs = new JsonObject(); + historyArgs.addProperty("limit", 3); + historyParams.add("arguments", historyArgs); + historyRequest.add("params", historyParams); + historyRequest.addProperty("id", 3); + + JsonObject historyResult = server.processTestRequest(historyRequest); + addLog("History result: " + historyResult.toString()); + + addLog("All tests completed successfully!"); + + } catch (Exception e) { + addLog("Test failed: " + e.getMessage()); + e.printStackTrace(); + } + }); + + testThread.setDaemon(true); + testThread.start(); + } + private void addLog(String message) { if (logArea != null) { javax.swing.SwingUtilities.invokeLater(() -> { From da28aac865d11890f21e5194e7f97899573c77b3 Mon Sep 17 00:00:00 2001 From: kakira Date: Sun, 3 Aug 2025 20:03:22 +0900 Subject: [PATCH 03/39] =?UTF-8?q?HTTP=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-setting-guide.md | 248 ++++++++++++++++++ scripts/mcp-http-bridge.js | 243 +++++++++++++++++ scripts/mcp-server.sh | 19 ++ scripts/mcp_server.py | 146 +++++++++++ .../extensions/mcp/MCPServerExtension.java | 101 ++++++- 5 files changed, 753 insertions(+), 4 deletions(-) create mode 100644 doc/mcp-server-setting-guide.md create mode 100755 scripts/mcp-http-bridge.js create mode 100755 scripts/mcp-server.sh create mode 100755 scripts/mcp_server.py diff --git a/doc/mcp-server-setting-guide.md b/doc/mcp-server-setting-guide.md new file mode 100644 index 00000000..8a16321d --- /dev/null +++ b/doc/mcp-server-setting-guide.md @@ -0,0 +1,248 @@ +# PacketProxy MCP サーバー設定ガイド + +## 概要 + +このガイドでは、PacketProxy MCP サーバーをClaude Desktopから使用するための設定方法を説明します。 + +**🔄 HTTP ベースアプローチ**: このガイドではHTTPベースのMCP接続を使用し、PacketProxy GUI内のMCPサーバーを直接利用します。これによりPythonの依存関係問題を回避できます。 + +## 前提条件 + +- PacketProxy がビルド済み (`./gradlew build` 実行済み) +- Claude Desktop がインストール済み +- Node.js がインストール済み (npxコマンド用) +- Java 17以降がインストール済み + +## 設定手順 + +### 1. PacketProxy GUIの起動とMCPサーバーの有効化 + +まず PacketProxy GUI を起動し、MCP サーバーを有効にします: + +```bash +# PacketProxyを起動 +java -jar build/libs/PacketProxy.jar +``` + +GUI起動後: +1. **Options** → **Extensions** を選択 +2. **MCP Server** を選択 +3. **Enable** にチェックを入れる +4. **Start Server** ボタンをクリック + +ログに以下が表示されることを確認: +``` +MCP Server started +HTTP endpoint available at http://localhost:8765/mcp +``` + +### 2. Claude Desktop設定ファイルの編集 + +Claude Desktopの設定ファイルを編集します: + +```bash +# 設定ファイルの場所 +open ~/Library/Application\ Support/Claude/claude_desktop_config.json +``` + +以下の内容を追加してください: + +```json +{ + "mcpServers": { + "packetproxy": { + "command": "node", + "args": [ + "/Users/kakira/PacketProxy/scripts/mcp-http-bridge.js" + ] + } + } +} +``` + +**既存の設定がある場合の例:** + +```json +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/directory"] + }, + "packetproxy": { + "command": "node", + "args": [ + "/Users/kakira/PacketProxy/scripts/mcp-http-bridge.js" + ] + } + } +} +``` + +### 4. Claude Desktopの再起動 + +設定を反映するためにClaude Desktopを完全に終了して再起動してください: + +1. Claude Desktop → Quit Claude +2. Claude Desktopを再起動 + +## 使用方法 + +### PacketProxyとClaude Desktopの連携テスト + +**前提条件:** +1. PacketProxy GUIが起動している +2. MCP Serverが有効化され、起動している +3. Claude Desktopが再起動済み + +Claude Desktopで新しい会話を開始し、以下を試してください: + +``` +PacketProxyのツールを使って、利用可能な機能を教えてください +``` + +**期待される応答:** +- `get_history`: パケット履歴の取得 +- `get_configs`: 設定情報の取得 +- `get_packet_detail`: パケット詳細情報の取得 + +### 実際のPacketProxy操作 + +PacketProxyでトラフィックをキャプチャした後、Claude Desktopから操作できます: + +``` +PacketProxyからパケット履歴を5件取得してください +``` + +``` +PacketProxyの設定情報を確認してください +``` + +``` +パケットID 1の詳細を取得してください +``` + +## トラブルシューティング + +### 設定確認 + +**1. 設定ファイルの確認** + +```bash +cat "/Users/kakira/Library/Application Support/Claude/claude_desktop_config.json" +# packetproxyの設定にnpxと@modelcontextprotocol/server-fetchが含まれていることを確認 +``` + +**2. PacketProxyのHTTPエンドポイント確認** + +```bash +# PacketProxy GUIでMCPサーバーが起動している場合 +curl -X POST http://localhost:8765/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}' +``` + +**3. Node.jsとnpxの確認** + +```bash +node --version +npx --version +# 両方のコマンドがバージョンを返すことを確認 +``` + +### よくある問題と対処法 + +**1. Claude Desktopでツールが認識されない** +- Claude Desktopを完全に再起動 +- 設定ファイルのJSON構文を確認 +- PacketProxy GUIでMCPサーバーが起動していることを確認 + +**2. HTTP接続エラー** + +```bash +# PacketProxyのHTTPエンドポイントが応答するか確認 +curl -v http://localhost:8765/mcp + +# ポート8765が使用中か確認 +lsof -i :8765 +``` + +**3. Node.jsブリッジのテスト** + +```bash +# ブリッジが正常に動作するかテスト +echo '{"jsonrpc": "2.0", "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0"}}, "id": 0}' | node /Users/kakira/PacketProxy/scripts/mcp-http-bridge.js +``` + +**4. PacketProxy JARファイルが見つからない** + +```bash +# ビルドを実行 +cd /Users/kakira/PacketProxy +./gradlew build + +# JARファイルの存在確認 +ls -la build/libs/PacketProxy.jar +``` + +## 利用可能なツール + +### `get_history` + +PacketProxyのパケット履歴を取得します。 + +**パラメータ:** +- `limit` (integer, optional): 取得するパケット数 (デフォルト: 100) +- `offset` (integer, optional): オフセット (デフォルト: 0) + +**使用例:** + +``` +最新のパケット履歴を5件取得してください +``` + +### `get_configs` + +PacketProxyの設定情報を取得します。 + +**パラメータ:** +- `categories` (array, optional): 取得する設定カテゴリ + +**使用例:** + +``` +PacketProxyの現在の設定を確認してください +``` + +### `get_packet_detail` + +特定のパケットの詳細情報を取得します。 + +**パラメータ:** +- `packet_id` (integer, required): パケットID +- `include_body` (boolean, optional): ボディを含めるか + +**使用例:** + +``` +パケットID 123の詳細情報を取得してください +``` + +## 今後の拡張予定 + +- 完全なGUI連携機能 +- パケット再送機能 (`resend_packet`) +- 設定変更機能 (`update_config`) +- フィルタ機能 (`get_filters`, `validate_filter`) +- HTTP REST API対応 + +## サポート + +問題が発生した場合は、以下を確認してください: + +1. PacketProxyのビルドが成功していること +2. Claude Desktopの設定ファイルが正しい形式であること +3. スクリプトファイルに実行権限があること +4. Python3が正常に動作すること + +詳細な技術仕様については `doc/mcp-server-spec.md` を参照してください。 diff --git a/scripts/mcp-http-bridge.js b/scripts/mcp-http-bridge.js new file mode 100755 index 00000000..f7033b87 --- /dev/null +++ b/scripts/mcp-http-bridge.js @@ -0,0 +1,243 @@ +#!/usr/bin/env node + +/** + * MCP HTTP Bridge + * Bridges HTTP MCP requests to PacketProxy's HTTP endpoint + */ + +const http = require('http'); +const { spawn } = require('child_process'); + +// Configuration +const PACKETPROXY_HTTP_URL = 'http://localhost:8765/mcp'; + +// MCP Server implementation +class MCPHttpBridge { + constructor() { + this.initialized = false; + this.serverInfo = { + name: "PacketProxy MCP Bridge", + version: "1.0.0" + }; + } + + async handleRequest(request) { + console.error(`[DEBUG] Processing request: ${request.method}`); + + switch (request.method) { + case 'initialize': + return this.handleInitialize(request); + case 'notifications/initialized': + return this.handleNotificationInitialized(request); + case 'tools/list': + return this.handleToolsList(request); + case 'tools/call': + return this.handleToolsCall(request); + case 'resources/list': + return this.handleResourcesList(request); + case 'prompts/list': + return this.handlePromptsList(request); + default: + return this.createErrorResponse(request.id, -32601, `Method not found: ${request.method}`); + } + } + + async handleInitialize(request) { + this.initialized = true; + console.error('[DEBUG] Initialize request processed'); + + return { + jsonrpc: "2.0", + id: request.id, + result: { + protocolVersion: "2024-11-05", + capabilities: { + tools: {} + }, + serverInfo: this.serverInfo + } + }; + } + + async handleNotificationInitialized(request) { + console.error('[DEBUG] Notification initialized received (no response needed)'); + // Notifications don't require a response, return null + return null; + } + + async handleResourcesList(request) { + console.error('[DEBUG] Resources list request - forwarding to PacketProxy'); + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } + } + + async handlePromptsList(request) { + console.error('[DEBUG] Prompts list request - forwarding to PacketProxy'); + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } + } + + async handleToolsList(request) { + if (!this.initialized) { + return this.createErrorResponse(request.id, -32002, "Server not initialized"); + } + + console.error('[DEBUG] Tools list request - forwarding to PacketProxy'); + + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } + } + + async handleToolsCall(request) { + if (!this.initialized) { + return this.createErrorResponse(request.id, -32002, "Server not initialized"); + } + + console.error(`[DEBUG] Tools call request: ${request.params?.name}`); + + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } + } + + async forwardToPacketProxy(request) { + return new Promise((resolve, reject) => { + const postData = JSON.stringify(request); + + console.error(`[DEBUG] Forwarding to PacketProxy: ${postData}`); + + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData) + } + }; + + const req = http.request(PACKETPROXY_HTTP_URL, options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const response = JSON.parse(data); + console.error(`[DEBUG] PacketProxy response: ${data}`); + resolve(response); + } catch (error) { + reject(new Error(`Failed to parse PacketProxy response: ${error.message}`)); + } + }); + }); + + req.on('error', (error) => { + reject(new Error(`HTTP request failed: ${error.message}`)); + }); + + req.write(postData); + req.end(); + }); + } + + createErrorResponse(id, code, message) { + return { + jsonrpc: "2.0", + id: id, + error: { + code: code, + message: message + } + }; + } +} + +// Main execution +async function main() { + console.error('[DEBUG] PacketProxy MCP HTTP Bridge starting...'); + + const bridge = new MCPHttpBridge(); + + process.stdin.setEncoding('utf8'); + + let buffer = ''; + + process.stdin.on('data', async (chunk) => { + buffer += chunk; + + // Process complete lines + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // Keep incomplete line in buffer + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) continue; + + try { + console.error(`[DEBUG] Received: ${trimmedLine}`); + const request = JSON.parse(trimmedLine); + const response = await bridge.handleRequest(request); + + // Only send response if it's not null (notifications don't need responses) + if (response !== null) { + const responseStr = JSON.stringify(response); + console.log(responseStr); + console.error(`[DEBUG] Sent: ${responseStr}`); + } else { + console.error(`[DEBUG] No response needed (notification)`); + } + } catch (error) { + console.error(`[ERROR] Failed to process request: ${error.message}`); + const errorResponse = { + jsonrpc: "2.0", + id: null, + error: { + code: -32700, + message: "Parse error" + } + }; + console.log(JSON.stringify(errorResponse)); + } + } + }); + + process.stdin.on('end', () => { + console.error('[DEBUG] Bridge shutting down'); + process.exit(0); + }); + + // Handle process termination + process.on('SIGINT', () => { + console.error('[DEBUG] Received SIGINT, shutting down'); + process.exit(0); + }); +} + +if (require.main === module) { + main().catch((error) => { + console.error(`[FATAL] ${error.message}`); + process.exit(1); + }); +} + +module.exports = MCPHttpBridge; \ No newline at end of file diff --git a/scripts/mcp-server.sh b/scripts/mcp-server.sh new file mode 100755 index 00000000..80e5d242 --- /dev/null +++ b/scripts/mcp-server.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# PacketProxy MCP Server launcher script +# This script starts PacketProxy in headless mode with MCP server enabled + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKETPROXY_DIR="$(dirname "$SCRIPT_DIR")" +JAR_FILE="$PACKETPROXY_DIR/build/libs/PacketProxy.jar" + +# Check if PacketProxy jar exists +if [ ! -f "$JAR_FILE" ]; then + echo "Error: PacketProxy.jar not found at $JAR_FILE" >&2 + echo "Please run 'gradlew build' first" >&2 + exit 1 +fi + +# Start PacketProxy with MCP server in headless mode +# We need to start PacketProxy GUI and enable MCP server programmatically +java -jar "$JAR_FILE" --mcp-server-mode diff --git a/scripts/mcp_server.py b/scripts/mcp_server.py new file mode 100755 index 00000000..eee8beea --- /dev/null +++ b/scripts/mcp_server.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +PacketProxy MCP Server +Provides MCP tools for interacting with PacketProxy +""" + +import sys +import os +import subprocess +import json +from pathlib import Path +from typing import Optional, List, Dict, Any + +# Add stderr logging for debugging +def log_debug(message: str): + print(f"DEBUG: {message}", file=sys.stderr, flush=True) + +try: + from mcp.server.fastmcp import FastMCP + log_debug("MCP SDK imported successfully") +except ImportError as e: + log_debug(f"Failed to import MCP SDK: {e}") + sys.exit(1) + +# Create MCP server +mcp = FastMCP("PacketProxy MCP Server") +log_debug("FastMCP server created") + +# Get PacketProxy directory +script_dir = Path(__file__).parent +packetproxy_dir = script_dir.parent +jar_file = packetproxy_dir / "build" / "libs" / "PacketProxy.jar" + +log_debug(f"PacketProxy directory: {packetproxy_dir}") +log_debug(f"JAR file path: {jar_file}") + +if not jar_file.exists(): + log_debug(f"Error: PacketProxy.jar not found at {jar_file}") + log_debug("Please run './gradlew build' first") + sys.exit(1) + +log_debug("PacketProxy.jar found") + +@mcp.tool() +def get_history(limit: Optional[int] = 100, offset: Optional[int] = 0) -> Dict[str, Any]: + """ + Get packet history from PacketProxy + + Args: + limit: Maximum number of packets to return (default: 100) + offset: Number of packets to skip (default: 0) + + Returns: + Dictionary containing packet history data + """ + log_debug(f"get_history called with limit={limit}, offset={offset}") + + # For now, return mock data indicating connection is working + return { + "status": "connected", + "message": "PacketProxy MCP server is working. To get real packet data, ensure PacketProxy GUI is running with MCP Server extension enabled.", + "limit": limit, + "offset": offset, + "packets": [ + { + "id": 1, + "method": "GET", + "url": "https://example.com/api/test", + "status": 200, + "timestamp": "2025-08-02T06:30:00Z" + } + ] + } + +@mcp.tool() +def get_configs(categories: Optional[List[str]] = None) -> Dict[str, Any]: + """ + Get PacketProxy configuration settings + + Args: + categories: List of configuration categories to retrieve + + Returns: + Dictionary containing configuration data + """ + log_debug(f"get_configs called with categories={categories}") + + return { + "status": "connected", + "message": "PacketProxy MCP server is working. To get real configuration data, ensure PacketProxy GUI is running with MCP Server extension enabled.", + "categories": categories or ["all"], + "configs": { + "listenPorts": [ + {"port": 8080, "protocol": "HTTP"} + ], + "servers": [ + {"name": "test-server", "host": "localhost", "port": 3000} + ] + } + } + +@mcp.tool() +def get_packet_detail(packet_id: int, include_body: Optional[bool] = False) -> Dict[str, Any]: + """ + Get detailed information for a specific packet + + Args: + packet_id: ID of the packet to retrieve + include_body: Whether to include packet body content + + Returns: + Dictionary containing detailed packet information + """ + log_debug(f"get_packet_detail called with packet_id={packet_id}, include_body={include_body}") + + return { + "status": "connected", + "message": "PacketProxy MCP server is working. To get real packet details, ensure PacketProxy GUI is running with MCP Server extension enabled.", + "packet_id": packet_id, + "include_body": include_body, + "packet": { + "id": packet_id, + "method": "GET", + "url": "https://example.com/api/test", + "headers": { + "User-Agent": "Mozilla/5.0", + "Accept": "application/json" + }, + "body": "Example response body" if include_body else None + } + } + +def main(): + log_debug("Starting PacketProxy MCP Server...") + + # Run the MCP server + try: + log_debug("About to call mcp.run()") + mcp.run() + log_debug("mcp.run() completed") + except Exception as e: + log_debug(f"Error running MCP server: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java index 1fc72ea5..21bb68c6 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java @@ -3,8 +3,16 @@ import static packetproxy.util.Logging.log; import com.google.gson.JsonObject; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JComponent; @@ -18,11 +26,13 @@ public class MCPServerExtension extends Extension { private MCPServer server; + private HttpServer httpServer; private JTextArea logArea; private JButton startButton; private JButton stopButton; private JButton testButton; private boolean isRunning = false; + private static final int HTTP_PORT = 8765; public MCPServerExtension() { super(); @@ -99,6 +109,13 @@ private void startServer() { try { server = new MCPServer(this::addLog); + + // Start HTTP server for MCP + httpServer = HttpServer.create(new InetSocketAddress(HTTP_PORT), 0); + httpServer.createContext("/mcp", new MCPHttpHandler()); + httpServer.setExecutor(null); // creates a default executor + httpServer.start(); + Thread serverThread = new Thread(() -> { try { server.run(); @@ -115,7 +132,8 @@ private void startServer() { stopButton.setEnabled(true); testButton.setEnabled(true); addLog("MCP Server started"); - log("MCP Server started"); + addLog("HTTP endpoint available at http://localhost:" + HTTP_PORT + "/mcp"); + log("MCP Server started with HTTP endpoint on port " + HTTP_PORT); } catch (Exception e) { addLog("Failed to start server: " + e.getMessage()); @@ -124,13 +142,21 @@ private void startServer() { } private void stopServer() { - if (!isRunning || server == null) { + if (!isRunning) { return; } try { - server.stop(); - server = null; + if (server != null) { + server.stop(); + server = null; + } + + if (httpServer != null) { + httpServer.stop(0); + httpServer = null; + } + isRunning = false; startButton.setEnabled(true); stopButton.setEnabled(false); @@ -216,4 +242,71 @@ private void addLog(String message) { }); } } + + // HTTP handler for MCP requests + private class MCPHttpHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + addLog("Received HTTP request: " + exchange.getRequestMethod() + " " + exchange.getRequestURI()); + + // Enable CORS + exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); + exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "POST, OPTIONS"); + exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type"); + + if ("OPTIONS".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + return; + } + + if (!"POST".equals(exchange.getRequestMethod())) { + String response = "Only POST method is supported"; + exchange.sendResponseHeaders(405, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + return; + } + + try { + // Read request body + InputStream is = exchange.getRequestBody(); + String requestBody = new String(is.readAllBytes(), StandardCharsets.UTF_8); + addLog("Request body: " + requestBody); + + // Process MCP request if server is available + String responseBody; + if (server != null) { + try { + JsonObject request = com.google.gson.JsonParser.parseString(requestBody).getAsJsonObject(); + JsonObject result = server.processTestRequest(request); + responseBody = result.toString(); + addLog("Response: " + responseBody); + } catch (Exception e) { + addLog("Error processing request: " + e.getMessage()); + responseBody = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"Internal error: " + + e.getMessage() + "\"},\"id\":null}"; + } + } else { + responseBody = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32002,\"message\":\"Server not available\"},\"id\":null}"; + } + + // Send response + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, responseBody.getBytes(StandardCharsets.UTF_8).length); + OutputStream os = exchange.getResponseBody(); + os.write(responseBody.getBytes(StandardCharsets.UTF_8)); + os.close(); + + } catch (Exception e) { + addLog("HTTP handler error: " + e.getMessage()); + String errorResponse = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32700,\"message\":\"Parse error\"},\"id\":null}"; + exchange.sendResponseHeaders(500, errorResponse.length()); + OutputStream os = exchange.getResponseBody(); + os.write(errorResponse.getBytes()); + os.close(); + } + } + } } From f51750f5bbcfe4eb93321a8e4f7c1f24bc14bc34 Mon Sep 17 00:00:00 2001 From: kakira Date: Sun, 3 Aug 2025 21:03:19 +0900 Subject: [PATCH 04/39] =?UTF-8?q?=E5=9F=BA=E6=9C=ACTool=E3=81=AE=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/mcp-http-bridge.js | 31 ++++++++------- .../packetproxy/extensions/mcp/MCPServer.java | 38 ++++++++++++++++++- .../extensions/mcp/tools/ConfigTool.java | 15 +++++++- .../extensions/mcp/tools/HistoryTool.java | 20 ++++++++-- .../mcp/tools/PacketDetailTool.java | 14 ++++++- 5 files changed, 97 insertions(+), 21 deletions(-) diff --git a/scripts/mcp-http-bridge.js b/scripts/mcp-http-bridge.js index f7033b87..101733e3 100755 --- a/scripts/mcp-http-bridge.js +++ b/scripts/mcp-http-bridge.js @@ -44,19 +44,15 @@ class MCPHttpBridge { async handleInitialize(request) { this.initialized = true; - console.error('[DEBUG] Initialize request processed'); + console.error('[DEBUG] Initialize request - forwarding to PacketProxy'); - return { - jsonrpc: "2.0", - id: request.id, - result: { - protocolVersion: "2024-11-05", - capabilities: { - tools: {} - }, - serverInfo: this.serverInfo - } - }; + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward initialize request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } } async handleNotificationInitialized(request) { @@ -208,9 +204,18 @@ async function main() { } } catch (error) { console.error(`[ERROR] Failed to process request: ${error.message}`); + // Try to extract ID from the malformed request + let requestId = null; + try { + const partialRequest = JSON.parse(trimmedLine); + requestId = partialRequest.id || null; + } catch (e) { + // If we can't parse at all, use null ID + } + const errorResponse = { jsonrpc: "2.0", - id: null, + id: requestId, error: { code: -32700, message: "Parse error" diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java index c3e9c044..c030f848 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java @@ -130,7 +130,18 @@ private void processRequest(String requestLine) { // Invalid JSON request JsonObject errorResponse = new JsonObject(); errorResponse.addProperty("jsonrpc", "2.0"); - errorResponse.add("id", null); + + // Try to extract ID from the malformed request if possible + JsonElement requestId = null; + try { + JsonObject partialRequest = JsonParser.parseString(requestLine).getAsJsonObject(); + if (partialRequest.has("id")) { + requestId = partialRequest.get("id"); + } + } catch (Exception parseError) { + // If we can't parse at all, use null ID + } + errorResponse.add("id", requestId); JsonObject error = new JsonObject(); error.addProperty("code", -32700); @@ -150,6 +161,10 @@ private JsonObject handleMethod(String method, JsonObject params) throws Excepti return handleToolsList(); case "tools/call" : return handleToolsCall(params); + case "resources/list" : + return handleResourcesList(); + case "prompts/list" : + return handlePromptsList(); default : throw new Exception("Unknown method: " + method); } @@ -163,8 +178,13 @@ private JsonObject handleInitialize(JsonObject params) { tools.addProperty("listChanged", true); capabilities.add("tools", tools); + JsonObject serverInfo = new JsonObject(); + serverInfo.addProperty("name", "PacketProxy MCP Server"); + serverInfo.addProperty("version", "1.0.0"); + result.add("capabilities", capabilities); - result.addProperty("serverInfo", "PacketProxy MCP Server v1.0"); + result.addProperty("protocolVersion", "2024-11-05"); + result.add("serverInfo", serverInfo); logger.accept("Client initialized"); return result; @@ -186,4 +206,18 @@ private JsonObject handleToolsCall(JsonObject params) throws Exception { return toolRegistry.callTool(toolName, arguments); } + + private JsonObject handleResourcesList() { + JsonObject result = new JsonObject(); + JsonObject[] resources = {}; + result.add("resources", gson.toJsonTree(resources)); + return result; + } + + private JsonObject handlePromptsList() { + JsonObject result = new JsonObject(); + JsonObject[] prompts = {}; + result.add("prompts", gson.toJsonTree(prompts)); + return result; + } } diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java index 5f241af3..58062c08 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java @@ -2,6 +2,7 @@ import static packetproxy.util.Logging.log; +import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import java.util.List; @@ -16,6 +17,8 @@ public class ConfigTool implements MCPTool { + private final Gson gson = new Gson(); + @Override public String getName() { return "get_configs"; @@ -77,8 +80,18 @@ public JsonObject call(JsonObject arguments) throws Exception { result.add("sslPassThroughs", getSSLPassThroughs()); } + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", gson.toJson(result)); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject mcpResult = new JsonObject(); + mcpResult.add("content", contentArray); + log("ConfigTool returning configuration"); - return result; + return mcpResult; } catch (Exception e) { log("ConfigTool error: " + e.getMessage()); diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java index f2850a0d..7d1bc770 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java @@ -2,6 +2,7 @@ import static packetproxy.util.Logging.log; +import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import java.text.SimpleDateFormat; @@ -12,6 +13,7 @@ public class HistoryTool implements MCPTool { private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private final Gson gson = new Gson(); @Override public String getName() { @@ -61,7 +63,6 @@ public JsonObject call(JsonObject arguments) throws Exception { Packets packets = Packets.getInstance(); List allPackets = packets.queryAll(); - JsonObject result = new JsonObject(); JsonArray packetsArray = new JsonArray(); int totalCount = allPackets.size(); @@ -74,9 +75,20 @@ public JsonObject call(JsonObject arguments) throws Exception { packetsArray.add(packetJson); } - result.add("packets", packetsArray); - result.addProperty("total_count", totalCount); - result.addProperty("has_more", endIndex < totalCount); + JsonObject data = new JsonObject(); + data.add("packets", packetsArray); + data.addProperty("total_count", totalCount); + data.addProperty("has_more", endIndex < totalCount); + + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", gson.toJson(data)); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject result = new JsonObject(); + result.add("content", contentArray); log("HistoryTool returning " + packetsArray.size() + " packets"); return result; diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java index 5436e295..3d750da2 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java @@ -2,6 +2,7 @@ import static packetproxy.util.Logging.log; +import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import java.nio.charset.StandardCharsets; @@ -12,6 +13,7 @@ public class PacketDetailTool implements MCPTool { private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private final Gson gson = new Gson(); @Override public String getName() { @@ -60,7 +62,17 @@ public JsonObject call(JsonObject arguments) throws Exception { throw new Exception("Packet not found: " + packetId); } - JsonObject result = buildPacketDetail(packet, includeBody); + JsonObject data = buildPacketDetail(packet, includeBody); + + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", gson.toJson(data)); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject result = new JsonObject(); + result.add("content", contentArray); log("PacketDetailTool returning packet " + packetId); return result; From d0e6a8b7be5d8a0d8d4be9e3ccd0e5f64085d602 Mon Sep 17 00:00:00 2001 From: kakira Date: Sun, 3 Aug 2025 21:48:03 +0900 Subject: [PATCH 05/39] =?UTF-8?q?filter=E6=A9=9F=E8=83=BD=E3=81=AE?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/mcp/tools/HistoryTool.java | 134 +++++++- .../extensions/mcp/tools/LogTool.java | 301 ++++++++++++++++++ .../extensions/mcp/tools/ToolRegistry.java | 3 +- .../java/core/packetproxy/gui/GUILog.java | 11 + 4 files changed, 444 insertions(+), 5 deletions(-) create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java index 7d1bc770..d9de0a61 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java @@ -7,8 +7,11 @@ import com.google.gson.JsonObject; import java.text.SimpleDateFormat; import java.util.List; +import java.util.ArrayList; +import javax.swing.RowFilter; import packetproxy.model.Packet; import packetproxy.model.Packets; +import packetproxy.common.FilterTextParser; public class HistoryTool implements MCPTool { @@ -41,6 +44,16 @@ public JsonObject getInputSchema() { offsetProp.addProperty("default", 0); schema.add("offset", offsetProp); + JsonObject filterProp = new JsonObject(); + filterProp.addProperty("type", "string"); + filterProp.addProperty("description", + "PacketProxy Filter syntax for filtering packets. " + + "Available columns: id, request, response, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, alpn, group, full_text, full_text_i, method, url, status. " + + "Operators: == (equals), != (not equals), >= (greater or equal), <= (less or equal), =~ (regex match), !~ (regex not match), && (AND), || (OR). " + + "Examples: 'method == GET', 'status >= 400', 'url =~ /api/', 'method == POST && status >= 400', 'length > 1000', 'full_text_i =~ authorization'" + ); + schema.add("filter", filterProp); + return schema; } @@ -50,6 +63,7 @@ public JsonObject call(JsonObject arguments) throws Exception { int limit = arguments.has("limit") ? arguments.get("limit").getAsInt() : 100; int offset = arguments.has("offset") ? arguments.get("offset").getAsInt() : 0; + String filter = arguments.has("filter") ? arguments.get("filter").getAsString() : null; // Validate parameters if (limit < 1 || limit > 1000) { @@ -62,15 +76,21 @@ public JsonObject call(JsonObject arguments) throws Exception { try { Packets packets = Packets.getInstance(); List allPackets = packets.queryAll(); + List filteredPackets = allPackets; + + // Apply filter if provided + if (filter != null && !filter.trim().isEmpty()) { + filteredPackets = applyFilter(allPackets, filter); + } JsonArray packetsArray = new JsonArray(); - int totalCount = allPackets.size(); + int totalCount = filteredPackets.size(); int startIndex = Math.min(offset, totalCount); int endIndex = Math.min(startIndex + limit, totalCount); for (int i = startIndex; i < endIndex; i++) { - Packet packet = allPackets.get(i); + Packet packet = filteredPackets.get(i); JsonObject packetJson = convertPacketToJson(packet); packetsArray.add(packetJson); } @@ -79,6 +99,9 @@ public JsonObject call(JsonObject arguments) throws Exception { data.add("packets", packetsArray); data.addProperty("total_count", totalCount); data.addProperty("has_more", endIndex < totalCount); + if (filter != null && !filter.trim().isEmpty()) { + data.addProperty("filter_applied", filter); + } JsonObject content = new JsonObject(); content.addProperty("type", "text"); @@ -90,7 +113,7 @@ public JsonObject call(JsonObject arguments) throws Exception { JsonObject result = new JsonObject(); result.add("content", contentArray); - log("HistoryTool returning " + packetsArray.size() + " packets"); + log("HistoryTool returning " + packetsArray.size() + " packets (filtered from " + allPackets.size() + " total)"); return result; } catch (Exception e) { @@ -99,6 +122,98 @@ public JsonObject call(JsonObject arguments) throws Exception { } } + private List applyFilter(List packets, String filterText) throws Exception { + List filtered = new ArrayList<>(); + + try { + // Parse the filter using FilterTextParser + RowFilter rowFilter = FilterTextParser.parse(filterText); + + for (Packet packet : packets) { + // Create a mock table entry to test the filter + Object[] rowData = createRowDataFromPacket(packet); + MockTableEntry entry = new MockTableEntry(rowData); + + if (rowFilter.include(entry)) { + filtered.add(packet); + } + } + } catch (Exception e) { + log("Filter parsing error: " + e.getMessage()); + throw new Exception("Invalid filter syntax: " + e.getMessage()); + } + + return filtered; + } + + private Object[] createRowDataFromPacket(Packet packet) { + Object[] rowData = new Object[17]; // Based on columnMapper size + + rowData[0] = packet.getId(); // id + + // Extract request and response data + try { + String request = new String(packet.getDecodedData(), "UTF-8"); + rowData[1] = request; // request + rowData[2] = ""; // response (not available in current packet data) + } catch (Exception e) { + rowData[1] = ""; + rowData[2] = ""; + } + + rowData[3] = packet.getDecodedData().length; // length + rowData[4] = packet.getClientIP(); // client_ip + rowData[5] = packet.getClientPort(); // client_port + rowData[6] = packet.getServerIP(); // server_ip + rowData[7] = packet.getServerPort(); // server_port + rowData[8] = packet.getDate(); // time + rowData[9] = packet.getResend(); // resend + rowData[10] = packet.getModified(); // modified + rowData[11] = packet.getContentType(); // type + rowData[12] = packet.getEncoder(); // encode + rowData[13] = ""; // alpn (not available) + rowData[14] = packet.getGroup(); // group + rowData[15] = (String) rowData[1]; // full_text (same as request) + rowData[16] = ((String) rowData[1]).toLowerCase(); // full_text_i (lowercase) + + return rowData; + } + + // Mock table entry class for filter testing + private static class MockTableEntry extends RowFilter.Entry { + private final Object[] data; + + public MockTableEntry(Object[] data) { + this.data = data; + } + + @Override + public Object getModel() { + return null; + } + + @Override + public int getValueCount() { + return data.length; + } + + @Override + public Object getValue(int index) { + return index < data.length ? data[index] : null; + } + + @Override + public String getStringValue(int index) { + Object value = getValue(index); + return value != null ? value.toString() : ""; + } + + @Override + public Object getIdentifier() { + return null; + } + } + private JsonObject convertPacketToJson(Packet packet) { JsonObject packetJson = new JsonObject(); @@ -113,8 +228,9 @@ private JsonObject convertPacketToJson(Packet packet) { packetJson.addProperty("modified", packet.getModified()); packetJson.addProperty("type", packet.getContentType()); packetJson.addProperty("encode", packet.getEncoder()); + packetJson.addProperty("group", packet.getGroup()); - // HTTPの場合、methodとurlを抽出 + // HTTPの場合、methodとurlとstatusを抽出 try { String request = new String(packet.getDecodedData(), "UTF-8"); String[] lines = request.split("\n"); @@ -124,6 +240,16 @@ private JsonObject convertPacketToJson(Packet packet) { packetJson.addProperty("method", requestLine[0]); packetJson.addProperty("url", requestLine[1]); } + + // レスポンスの場合、ステータスコードを抽出 + if (requestLine.length >= 3 && requestLine[0].startsWith("HTTP/")) { + try { + int status = Integer.parseInt(requestLine[1]); + packetJson.addProperty("status", status); + } catch (NumberFormatException e) { + // ステータスコードが数値でない場合は無視 + } + } } } catch (Exception e) { // HTTP以外のパケットの場合は無視 diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java new file mode 100644 index 00000000..0a015267 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java @@ -0,0 +1,301 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import packetproxy.gui.GUILog; + +public class LogTool implements MCPTool { + + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); + private final Gson gson = new Gson(); + + @Override + public String getName() { + return "get_logs"; + } + + @Override + public String getDescription() { + return "Get logs from PacketProxy"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject levelProp = new JsonObject(); + levelProp.addProperty("type", "string"); + levelProp.addProperty("description", "Log level filter: debug, info, warn, error"); + levelProp.addProperty("default", "info"); + schema.add("level", levelProp); + + JsonObject limitProp = new JsonObject(); + limitProp.addProperty("type", "integer"); + limitProp.addProperty("description", "Maximum number of log entries to return"); + limitProp.addProperty("default", 100); + schema.add("limit", limitProp); + + JsonObject sinceProp = new JsonObject(); + sinceProp.addProperty("type", "string"); + sinceProp.addProperty("description", "Start time in ISO 8601 format (e.g., 2025-01-15T00:00:00Z)"); + schema.add("since", sinceProp); + + JsonObject filterProp = new JsonObject(); + filterProp.addProperty("type", "string"); + filterProp.addProperty("description", "Regular expression filter for log messages"); + schema.add("filter", filterProp); + + return schema; + } + + @Override + public JsonObject call(JsonObject arguments) throws Exception { + log("LogTool called with arguments: " + arguments.toString()); + + String level = arguments.has("level") ? arguments.get("level").getAsString() : "info"; + int limit = arguments.has("limit") ? arguments.get("limit").getAsInt() : 100; + String since = arguments.has("since") ? arguments.get("since").getAsString() : null; + String filter = arguments.has("filter") ? arguments.get("filter").getAsString() : null; + + // Validate parameters + if (limit < 1 || limit > 1000) { + throw new Exception("Limit must be between 1 and 1000"); + } + + if (!isValidLogLevel(level)) { + throw new Exception("Invalid log level. Use: debug, info, warn, error"); + } + + LocalDateTime sinceDateTime = null; + if (since != null) { + try { + sinceDateTime = LocalDateTime.parse(since.replace("Z", ""), + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); + } catch (DateTimeParseException e) { + throw new Exception("Invalid date format. Use ISO 8601 format (e.g., 2025-01-15T00:00:00Z)"); + } + } + + Pattern filterPattern = null; + if (filter != null && !filter.trim().isEmpty()) { + try { + filterPattern = Pattern.compile(filter, Pattern.CASE_INSENSITIVE); + } catch (Exception e) { + throw new Exception("Invalid regex pattern: " + e.getMessage()); + } + } + + try { + // 実際のログ取得処理 + // PacketProxyのログはutil.Loggingを通してGUILogに保存されているため、 + // そこからログエントリを取得する + List logEntries = getLogEntriesFromGUILog(level, sinceDateTime, filterPattern, limit); + + JsonArray logsArray = new JsonArray(); + for (LogEntry entry : logEntries) { + JsonObject logJson = new JsonObject(); + logJson.addProperty("timestamp", dateFormat.format(entry.getTimestamp())); + logJson.addProperty("level", entry.getLevel()); + logJson.addProperty("message", entry.getMessage()); + logJson.addProperty("thread", entry.getThread()); + logJson.addProperty("class", entry.getClassName()); + logsArray.add(logJson); + } + + JsonObject data = new JsonObject(); + data.add("logs", logsArray); + data.addProperty("total_count", logEntries.size()); + data.addProperty("has_more", logEntries.size() >= limit); + + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", gson.toJson(data)); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject result = new JsonObject(); + result.add("content", contentArray); + + log("LogTool returning " + logsArray.size() + " log entries"); + return result; + + } catch (Exception e) { + log("LogTool error: " + e.getMessage()); + throw new Exception("Failed to get logs: " + e.getMessage()); + } + } + + private boolean isValidLogLevel(String level) { + return level.equals("debug") || level.equals("info") || level.equals("warn") || level.equals("error"); + } + + private List getLogEntriesFromGUILog(String level, LocalDateTime since, Pattern filter, int limit) { + List entries = new ArrayList<>(); + + try { + GUILog guiLog = GUILog.getInstance(); + String logText = guiLog.getLogText(); + + if (logText != null && !logText.trim().isEmpty()) { + // ログテキストを行ごとに分析 + String[] lines = logText.split("\n"); + + for (String line : lines) { + if (line.trim().isEmpty()) { + continue; + } + + LogEntry entry = parseLogLine(line.trim()); + if (entry != null) { + entries.add(entry); + } + } + } + + // 最新のログが上に来るようにリバース + java.util.Collections.reverse(entries); + + } catch (Exception e) { + log("Error getting log entries: " + e.getMessage()); + } + + // フィルタリング適用 + List filteredEntries = new ArrayList<>(); + for (LogEntry entry : entries) { + // レベルフィルタ + if (!matchesLogLevel(entry.getLevel(), level)) { + continue; + } + + // 時間フィルタ + if (since != null) { + LocalDateTime entryTime = LocalDateTime.ofInstant( + entry.getTimestamp().toInstant(), ZoneId.systemDefault()); + if (entryTime.isBefore(since)) { + continue; + } + } + + // 正規表現フィルタ + if (filter != null && !filter.matcher(entry.getMessage()).find()) { + continue; + } + + filteredEntries.add(entry); + + // 制限チェック + if (filteredEntries.size() >= limit) { + break; + } + } + + return filteredEntries; + } + + private boolean matchesLogLevel(String entryLevel, String filterLevel) { + // レベルの優先度: debug < info < warn < error + int entryPriority = getLogLevelPriority(entryLevel); + int filterPriority = getLogLevelPriority(filterLevel); + return entryPriority >= filterPriority; + } + + private int getLogLevelPriority(String level) { + switch (level.toLowerCase()) { + case "debug": return 0; + case "info": return 1; + case "warn": return 2; + case "error": return 3; + default: return 1; // デフォルトはinfo + } + } + + private LogEntry parseLogLine(String line) { + try { + // PacketProxyのログ形式: "yyyy/MM/dd HH:mm:ss message" + // util.Loggingの形式に基づく + if (line.length() < 19) { + return null; // 最小の日時フォーマット長より短い + } + + String dateTimePart = line.substring(0, 19); + String messagePart = line.length() > 26 ? line.substring(26) : ""; + + // 日時をパース + java.util.Date timestamp; + try { + LocalDateTime localDateTime = LocalDateTime.parse(dateTimePart, dtf); + timestamp = java.util.Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + } catch (DateTimeParseException e) { + // 日時パースに失敗した場合は現在時刻を使用 + timestamp = new java.util.Date(); + } + + // ログレベルを推定(メッセージ内容から) + String level = "info"; // デフォルト + String lowerMessage = messagePart.toLowerCase(); + if (lowerMessage.contains("error") || lowerMessage.contains("exception") || + lowerMessage.contains("failed") || lowerMessage.contains("fail")) { + level = "error"; + } else if (lowerMessage.contains("warn") || lowerMessage.contains("warning")) { + level = "warn"; + } else if (lowerMessage.contains("debug")) { + level = "debug"; + } + + // スレッド名とクラス名を推定 + String thread = "main"; // デフォルト + String className = "packetproxy"; // デフォルト + + // メッセージからクラス名を抽出を試行 + if (messagePart.contains("MCP")) { + className = "packetproxy.extensions.mcp"; + } else if (messagePart.contains("Server")) { + className = "packetproxy.extensions.mcp.MCPServer"; + } else if (messagePart.contains("Tool")) { + className = "packetproxy.extensions.mcp.tools"; + } + + return new LogEntry(timestamp, level, messagePart, thread, className); + + } catch (Exception e) { + // パースに失敗した場合はnullを返す + return null; + } + } + + // ログエントリを表すクラス + private static class LogEntry { + private final java.util.Date timestamp; + private final String level; + private final String message; + private final String thread; + private final String className; + + public LogEntry(java.util.Date timestamp, String level, String message, String thread, String className) { + this.timestamp = timestamp; + this.level = level; + this.message = message; + this.thread = thread; + this.className = className; + } + + public java.util.Date getTimestamp() { return timestamp; } + public String getLevel() { return level; } + public String getMessage() { return message; } + public String getThread() { return thread; } + public String getClassName() { return className; } + } +} \ No newline at end of file diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java index 2b62d0f5..0a2e0b9e 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java @@ -15,10 +15,11 @@ public ToolRegistry() { } private void registerDefaultTools() { - // 基本的な3つのツールを登録 + // 基本的な4つのツールを登録 registerTool(new HistoryTool()); registerTool(new PacketDetailTool()); registerTool(new ConfigTool()); + registerTool(new LogTool()); } public void registerTool(MCPTool tool) { diff --git a/src/main/java/core/packetproxy/gui/GUILog.java b/src/main/java/core/packetproxy/gui/GUILog.java index 3a06d9ac..600c7fb7 100644 --- a/src/main/java/core/packetproxy/gui/GUILog.java +++ b/src/main/java/core/packetproxy/gui/GUILog.java @@ -83,4 +83,15 @@ public void appendErr(String s) { } } + + public String getLogText() { + synchronized (thread_lock) { + try { + StyledDocument doc = text.getStyledDocument(); + return doc.getText(0, doc.getLength()); + } catch (BadLocationException ex) { + return ""; + } + } + } } From f1c9cc7e469766dc06b2aa67913308102951c84f Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 4 Aug 2025 16:51:46 +0900 Subject: [PATCH 06/39] =?UTF-8?q?config=20=E5=91=A8=E3=82=8A=E3=81=AETool?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + doc/mcp-server-spec.md | 158 +++++++---- .../packetproxy/extensions/mcp/MCPServer.java | 15 +- .../extensions/mcp/MCPServerExtension.java | 2 +- .../mcp/tools/AuthenticatedMCPTool.java | 77 ++++++ .../extensions/mcp/tools/ConfigTool.java | 186 ++++++------- .../extensions/mcp/tools/HistoryTool.java | 6 +- .../extensions/mcp/tools/LogTool.java | 6 +- .../mcp/tools/PacketDetailTool.java | 6 +- .../mcp/tools/RestoreConfigTool.java | 128 +++++++++ .../extensions/mcp/tools/ToolRegistry.java | 4 +- .../mcp/tools/UpdateConfigTool.java | 259 ++++++++++++++++++ 12 files changed, 679 insertions(+), 170 deletions(-) create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java diff --git a/.gitignore b/.gitignore index 26f73ea1..a805ccb2 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ /dena/ /denaN/ /denaL/ + +/backup/ diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index dd74370d..f73f3914 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -26,6 +26,24 @@ PacketProxy MCP サーバーは、Model Context Protocol (MCP) を使用してPa └─────────────────┘ ``` +## 認証 + +**すべてのMCPツール**は認証が必要です。各ツールの呼び出し時に`access_token`パラメータを指定する必要があります。 + +### アクセストークンの取得方法 + +1. PacketProxyの**Settings**タブを開く +2. **Import/Export configs (Experimental)**セクションを見つける +3. **Enabled**チェックボックスを有効にする +4. 自動生成された**AccessToken**をコピーする +5. MCPツール呼び出し時に`access_token`パラメータとして使用する + +### 認証エラーの場合 + +- アクセストークンが未設定: PacketProxyでconfig sharingを有効にしてください +- アクセストークンが無効: Settings画面で正しいトークンを確認してください +- アクセストークンが空: 必須パラメータのため、必ず指定してください + ## MCPツール一覧 ### 1. `get_history` - パケット履歴取得 @@ -41,12 +59,10 @@ PacketProxyのパケット履歴を検索・取得します。 "params": { "name": "get_history", "arguments": { + "access_token": "your_access_token_here", "limit": 100, "offset": 0, - "filter": "method == GET && url =~ /api/", - "columns": ["id", "method", "url", "status", "length", "time"], - "sort_by": "time", - "sort_order": "desc" + "filter": "method == GET && url =~ /api/" } }, "id": 1 @@ -54,12 +70,10 @@ PacketProxyのパケット履歴を検索・取得します。 ``` **パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン - `limit` (number, optional): 取得件数 (デフォルト: 100) - `offset` (number, optional): オフセット (デフォルト: 0) - `filter` (string, optional): PacketProxy Filter構文による絞り込み -- `columns` (array, optional): 取得するカラム -- `sort_by` (string, optional): ソート対象カラム (デフォルト: time) -- `sort_order` (string, optional): "asc" | "desc" (デフォルト: desc) **レスポンス:** @@ -99,6 +113,7 @@ PacketProxyのパケット履歴を検索・取得します。 "params": { "name": "get_packet_detail", "arguments": { + "access_token": "your_access_token_here", "packet_id": 123, "include_body": true } @@ -108,6 +123,7 @@ PacketProxyのパケット履歴を検索・取得します。 ``` **パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン - `packet_id` (number, required): パケットID - `include_body` (boolean, optional): リクエスト/レスポンスボディを含める (デフォルト: false) @@ -144,9 +160,9 @@ PacketProxyのパケット履歴を検索・取得します。 } ``` -### 3. `get_configs` - 設定情報取得 +### 3. `get_config` - 設定情報取得 -PacketProxyの設定情報を取得します。 +PacketProxyの設定情報をHTTP API (`http://localhost:32349/config`) 経由で取得します。PacketProxyHub互換の完全な設定形式で返されます。 **リクエスト:** @@ -155,9 +171,10 @@ PacketProxyの設定情報を取得します。 "jsonrpc": "2.0", "method": "tools/call", "params": { - "name": "get_configs", + "name": "get_config", "arguments": { - "categories": ["listenPorts", "servers"] + "categories": ["listenPorts", "servers"], + "access_token": "your_access_token_here" } }, "id": 3 @@ -165,11 +182,12 @@ PacketProxyの設定情報を取得します。 ``` **パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン - `categories` (array, optional): 取得するカテゴリ (空の場合は全て) -- `listenPorts`: リッスンポート設定 -- `servers`: サーバー設定 -- `modifications`: 改変設定 -- `sslPassThroughs`: SSL パススルー設定 + - `listenPorts`: リッスンポート設定 + - `servers`: サーバー設定 + - `modifications`: 改変設定 + - `sslPassThroughs`: SSL パススルー設定 **レスポンス:** @@ -177,26 +195,12 @@ PacketProxyの設定情報を取得します。 { "jsonrpc": "2.0", "result": { - "listenPorts": [ + "content": [ { - "id": 1, - "port": 8080, - "protocol": "HTTP", - "serverId": 1, - "enabled": true - } - ], - "servers": [ - { - "id": 1, - "host": "target.com", - "port": 443, - "protocol": "HTTPS", - "enabled": true + "type": "text", + "text": "{\"listenPorts\":[{\"id\":1,\"enabled\":true,\"ca_name\":\"PacketProxy per-user CA\",\"port\":8080,\"type\":\"HTTP_PROXY\",\"server_id\":1}],\"servers\":[{\"id\":1,\"ip\":\"target.com\",\"port\":443,\"encoder\":\"HTTPS\",\"use_ssl\":true,\"resolved_by_dns\":false,\"resolved_by_dns6\":false,\"http_proxy\":false,\"comment\":\"\",\"specifiedByHostName\":false}]}" } - ], - "modifications": [], - "sslPassThroughs": [] + ] }, "id": 3 } @@ -204,7 +208,7 @@ PacketProxyの設定情報を取得します。 ### 4. `update_config` - 設定変更 -PacketProxyの設定を変更します。PacketProxyHub互換の形式を使用します。 +PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変更します。PacketProxyHub互換の形式を使用し、指定されたIDが含まれない項目は自動的に削除されます。 **リクエスト:** @@ -217,19 +221,51 @@ PacketProxyの設定を変更します。PacketProxyHub互換の形式を使用 "arguments": { "config_json": { "listenPorts": [ - {"id": 1, "port": 8080, "protocol": "HTTP", "serverId": 1} + { + "id": 1, + "enabled": true, + "ca_name": "PacketProxy per-user CA", + "port": 8080, + "type": "HTTP_PROXY", + "server_id": 1 + } ], "servers": [ - {"id": 1, "host": "target.com", "port": 443, "protocol": "HTTPS"} + { + "id": 1, + "ip": "target.com", + "port": 443, + "encoder": "HTTPS", + "use_ssl": true, + "resolved_by_dns": false, + "resolved_by_dns6": false, + "http_proxy": false, + "comment": "", + "specifiedByHostName": false + } ], "modifications": [ - {"id": 1, "name": "Add Header", "pattern": ".*", "replacement": "X-Test: 1"} + { + "id": 1, + "enabled": true, + "server_id": 1, + "direction": "CLIENT_REQUEST", + "pattern": ".*", + "method": "SIMPLE", + "replaced": "X-Test: 1" + } ], "sslPassThroughs": [ - {"id": 1, "host": "secure.com", "port": 443} + { + "id": 1, + "enabled": true, + "server_name": "secure.com", + "listen_port": 443 + } ] }, - "backup": true + "backup": true, + "access_token": "your_access_token_here" } }, "id": 4 @@ -237,23 +273,27 @@ PacketProxyの設定を変更します。PacketProxyHub互換の形式を使用 ``` **パラメータ:** -- `config_json` (object, required): PacketProxyHub互換の設定JSON +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `config_json` (object, required): PacketProxyHub互換の設定JSON(完全な形式) - `backup` (boolean, optional): 既存設定をバックアップ (デフォルト: true) +**設定削除について:** +- `config_json`に含まれないIDの項目は自動的に削除されます +- 例: serversに`id:1`のみ含まれている場合、`id:2,3...`のサーバーは削除されます +- HTTP APIは既存設定を完全に置き換える方式で動作します + **レスポンス:** ```json { "jsonrpc": "2.0", "result": { - "success": true, - "backup_created": true, - "backup_info": { - "backup_id": "backup_20250115_103000", - "backup_path": "/path/to/backups/config_backup_20250115_103000.json", - "timestamp": "2025-01-15T10:30:00Z" - }, - "config_updated": true + "content": [ + { + "type": "text", + "text": "{\"success\": true, \"backup_created\": true, \"backup_info\": {\"backup_id\": \"backup_20250804_120000\", \"backup_path\": \"backup/backup_20250804_120000.json\", \"timestamp\": \"2025-08-04T12:00:00Z\"}, \"config_updated\": true}" + } + ] }, "id": 4 } @@ -345,6 +385,7 @@ PacketProxyのログを取得します。 "params": { "name": "get_logs", "arguments": { + "access_token": "your_access_token_here", "level": "info", "limit": 100, "since": "2025-01-15T00:00:00Z", @@ -356,6 +397,7 @@ PacketProxyのログを取得します。 ``` **パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン - `level` (string, optional): ログレベル "debug" | "info" | "warn" | "error" - `limit` (number, optional): 取得件数 (デフォルト: 100) - `since` (string, optional): 開始時刻 (ISO 8601形式) @@ -565,7 +607,7 @@ PacketProxyのログを取得します。 } ``` -### 10. `restore_config_backup` - 設定バックアップ復元 +### 10. `restore_config` - 設定バックアップ復元 指定したバックアップから設定を復元します。 @@ -576,8 +618,9 @@ PacketProxyのログを取得します。 "jsonrpc": "2.0", "method": "tools/call", "params": { - "name": "restore_config_backup", + "name": "restore_config", "arguments": { + "access_token": "your_access_token_here", "backup_id": "backup_20250115_103000" } }, @@ -585,15 +628,22 @@ PacketProxyのログを取得します。 } ``` +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `backup_id` (string, required): 復元するバックアップID + **レスポンス:** ```json { "jsonrpc": "2.0", "result": { - "success": true, - "restored_from": "backup_20250115_103000", - "backup_created": "backup_20250115_104500" + "content": [ + { + "type": "text", + "text": "{\"success\": true, \"backup_id_restored\": \"backup_20250115_103000\", \"config_restored\": true}" + } + ] }, "id": 10 } @@ -677,7 +727,7 @@ GET /mcp/logs?level=info # ログ取得 GET /mcp/filters # フィルタ一覧 POST /mcp/filters/validate # フィルタ検証 GET /mcp/backups # バックアップ一覧 -POST /mcp/backups/{backup_id}/restore # バックアップ復元 +POST /mcp/restore/{backup_id} # バックアップ復元 ``` ### 認証 diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java index c030f848..48fb3dc7 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java @@ -79,8 +79,12 @@ public JsonObject processTestRequest(JsonObject request) throws Exception { JsonObject response = new JsonObject(); response.addProperty("jsonrpc", "2.0"); - if (id != null) { + // Always ensure ID is present - use original request ID or default + if (id != null && !id.isJsonNull()) { response.add("id", id); + } else { + // If no valid ID in request, use default ID + response.addProperty("id", 0); } try { @@ -91,7 +95,8 @@ public JsonObject processTestRequest(JsonObject request) throws Exception { error.addProperty("code", -32603); error.addProperty("message", "Internal error: " + e.getMessage()); response.add("error", error); - throw e; + // Don't throw exception - return error response instead + logger.accept("Method error: " + e.getMessage()); } return response; @@ -107,8 +112,12 @@ private void processRequest(String requestLine) { JsonObject response = new JsonObject(); response.addProperty("jsonrpc", "2.0"); - if (id != null) { + // Always ensure ID is present - use original request ID or default + if (id != null && !id.isJsonNull()) { response.add("id", id); + } else { + // If no valid ID in request, use default ID + response.addProperty("id", 0); } try { diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java index 21bb68c6..b68eb21f 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java @@ -197,7 +197,7 @@ private void testTools() { configRequest.addProperty("method", "tools/call"); JsonObject configParams = new JsonObject(); - configParams.addProperty("name", "get_configs"); + configParams.addProperty("name", "get_config"); configParams.add("arguments", new JsonObject()); configRequest.add("params", configParams); configRequest.addProperty("id", 2); diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java new file mode 100644 index 00000000..28420411 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java @@ -0,0 +1,77 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonObject; +import packetproxy.model.ConfigString; + +/** + * 認証機能付きMCPツールの基底クラス + */ +public abstract class AuthenticatedMCPTool implements MCPTool { + + /** + * AccessTokenの検証を行う + */ + protected void validateAccessToken(JsonObject arguments) throws Exception { + // MCP clientから渡されたAccessTokenを取得 + if (!arguments.has("access_token")) { + throw new Exception("access_token parameter is required. Please provide your PacketProxy access token from Settings."); + } + + String providedToken = arguments.get("access_token").getAsString(); + if (providedToken == null || providedToken.trim().isEmpty()) { + throw new Exception("access_token cannot be empty. Please provide your PacketProxy access token from Settings > Import/Export configs section."); + } + + // PacketProxy設定からAccessTokenを取得 + String configuredToken = new ConfigString("SharingConfigsAccessToken").getString(); + if (configuredToken.isEmpty()) { + throw new Exception("Access token not configured in PacketProxy. Please enable 'Import/Export configs' in PacketProxy Settings and copy the generated access token."); + } + + // トークンの照合 + if (!configuredToken.equals(providedToken)) { + log("Access token validation failed. Provided: " + providedToken + ", Expected: " + configuredToken); + throw new Exception("Invalid access token. Please check your access token from PacketProxy Settings > Import/Export configs section."); + } + + log("Access token validation successful"); + } + + /** + * 設定済みAccessTokenを取得(HTTPリクエスト用) + */ + protected String getConfiguredAccessToken() throws Exception { + String accessToken = new ConfigString("SharingConfigsAccessToken").getString(); + if (accessToken.isEmpty()) { + throw new Exception("Access token not configured. Please enable config sharing in settings."); + } + return accessToken; + } + + /** + * 入力スキーマにaccess_tokenパラメータを追加 + */ + protected JsonObject addAccessTokenToSchema(JsonObject schema) { + JsonObject accessTokenProp = new JsonObject(); + accessTokenProp.addProperty("type", "string"); + accessTokenProp.addProperty("description", "Access token for authentication"); + schema.add("access_token", accessTokenProp); + return schema; + } + + /** + * サブクラスで実装する認証後の実際の処理 + */ + protected abstract JsonObject executeAuthenticated(JsonObject arguments) throws Exception; + + /** + * 認証チェック付きでツールを実行 + */ + @Override + public final JsonObject call(JsonObject arguments) throws Exception { + validateAccessToken(arguments); + return executeAuthenticated(arguments); + } +} \ No newline at end of file diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java index 58062c08..64182642 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java @@ -6,22 +6,42 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import java.util.List; +import packetproxy.model.CharSet; +import packetproxy.model.CharSets; +import packetproxy.model.ClientCertificate; +import packetproxy.model.ClientCertificates; +import packetproxy.model.Config; +import packetproxy.model.Configs; +import packetproxy.model.Extension; +import packetproxy.model.Extensions; +import packetproxy.model.Filter; +import packetproxy.model.Filters; +import packetproxy.model.InterceptOption; +import packetproxy.model.InterceptOptions; import packetproxy.model.ListenPort; import packetproxy.model.ListenPorts; import packetproxy.model.Modification; import packetproxy.model.Modifications; +import packetproxy.model.OpenVPNForwardPort; +import packetproxy.model.OpenVPNForwardPorts; +import packetproxy.model.Resolution; +import packetproxy.model.Resolutions; import packetproxy.model.SSLPassThrough; import packetproxy.model.SSLPassThroughs; import packetproxy.model.Server; import packetproxy.model.Servers; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; -public class ConfigTool implements MCPTool { +public class ConfigTool extends AuthenticatedMCPTool { private final Gson gson = new Gson(); @Override public String getName() { - return "get_configs"; + return "get_config"; } @Override @@ -44,45 +64,37 @@ public JsonObject getInputSchema() { enumValues.add("servers"); enumValues.add("modifications"); enumValues.add("sslPassThroughs"); + enumValues.add("resolutions"); + enumValues.add("interceptOptions"); + enumValues.add("clientCertificates"); + enumValues.add("generalConfigs"); + enumValues.add("extensions"); + enumValues.add("filters"); + enumValues.add("openVPNForwardPorts"); + enumValues.add("charSets"); itemsProp.add("enum", enumValues); categoriesProp.add("items", itemsProp); schema.add("categories", categoriesProp); - return schema; + return addAccessTokenToSchema(schema); } @Override - public JsonObject call(JsonObject arguments) throws Exception { + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { log("ConfigTool called with arguments: " + arguments.toString()); - JsonArray categories = null; - if (arguments.has("categories")) { - categories = arguments.getAsJsonArray("categories"); - } - - JsonObject result = new JsonObject(); - try { - if (shouldIncludeCategory(categories, "listenPorts")) { - result.add("listenPorts", getListenPorts()); - } - - if (shouldIncludeCategory(categories, "servers")) { - result.add("servers", getServers()); - } - - if (shouldIncludeCategory(categories, "modifications")) { - result.add("modifications", getModifications()); - } - - if (shouldIncludeCategory(categories, "sslPassThroughs")) { - result.add("sslPassThroughs", getSSLPassThroughs()); - } + // HTTP APIで設定を取得 + String configJson = getConfigFromHttpApi(); + + // categoriesでフィルタリングが指定されている場合はフィルタリングを適用 + JsonObject allConfig = gson.fromJson(configJson, JsonObject.class); + JsonObject filteredConfig = filterByCategories(allConfig, arguments); JsonObject content = new JsonObject(); content.addProperty("type", "text"); - content.addProperty("text", gson.toJson(result)); + content.addProperty("text", gson.toJson(filteredConfig)); JsonArray contentArray = new JsonArray(); contentArray.add(content); @@ -90,7 +102,7 @@ public JsonObject call(JsonObject arguments) throws Exception { JsonObject mcpResult = new JsonObject(); mcpResult.add("content", contentArray); - log("ConfigTool returning configuration"); + log("ConfigTool returning configuration from HTTP API"); return mcpResult; } catch (Exception e) { @@ -98,86 +110,56 @@ public JsonObject call(JsonObject arguments) throws Exception { throw new Exception("Failed to get configuration: " + e.getMessage()); } } - - private boolean shouldIncludeCategory(JsonArray categories, String category) { - if (categories == null || categories.size() == 0) { - return true; // Include all if not specified + + private String getConfigFromHttpApi() throws Exception { + // 設定済みAccessTokenを取得(HTTPリクエスト用) + String accessToken = getConfiguredAccessToken(); + + // HTTP GETリクエスト + URL url = new URL("http://localhost:32349/config"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Authorization", accessToken); + conn.setConnectTimeout(5000); + conn.setReadTimeout(10000); + + int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + throw new Exception("HTTP API returned status: " + responseCode + ". Check if config sharing is enabled."); } - - for (int i = 0; i < categories.size(); i++) { - if (categories.get(i).getAsString().equals(category)) { - return true; - } + + // レスポンスを読み取り + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); } - return false; + reader.close(); + conn.disconnect(); + + return response.toString(); } - - private JsonArray getListenPorts() throws Exception { - JsonArray portsArray = new JsonArray(); - List ports = ListenPorts.getInstance().queryAll(); - - for (ListenPort port : ports) { - JsonObject portJson = new JsonObject(); - portJson.addProperty("id", port.getId()); - portJson.addProperty("port", port.getPort()); - portJson.addProperty("protocol", port.getProtocol().toString()); - portJson.addProperty("type", port.getType().toString()); - portJson.addProperty("serverId", port.getServerId()); - portJson.addProperty("enabled", port.isEnabled()); - portsArray.add(portJson); + + private JsonObject filterByCategories(JsonObject config, JsonObject arguments) { + if (!arguments.has("categories")) { + return config; // カテゴリ指定がない場合は全て返す } - - return portsArray; - } - - private JsonArray getServers() throws Exception { - JsonArray serversArray = new JsonArray(); - List servers = Servers.getInstance().queryAll(); - - for (Server server : servers) { - JsonObject serverJson = new JsonObject(); - serverJson.addProperty("id", server.getId()); - serverJson.addProperty("ip", server.getIp()); - serverJson.addProperty("port", server.getPort()); - serverJson.addProperty("encoder", server.getEncoder()); - serverJson.addProperty("use_ssl", server.getUseSSL()); - serverJson.addProperty("comment", server.getComment()); - serversArray.add(serverJson); + + JsonArray categories = arguments.getAsJsonArray("categories"); + if (categories.size() == 0) { + return config; // 空の場合は全て返す } - - return serversArray; - } - - private JsonArray getModifications() throws Exception { - JsonArray modificationsArray = new JsonArray(); - List modifications = Modifications.getInstance().queryAll(); - - for (Modification mod : modifications) { - JsonObject modJson = new JsonObject(); - modJson.addProperty("id", mod.getId()); - modJson.addProperty("direction", mod.getDirection().toString()); - modJson.addProperty("method", mod.getMethod().toString()); - modJson.addProperty("pattern", mod.getPattern()); - modJson.addProperty("replaced", mod.getReplaced()); - modJson.addProperty("serverId", mod.getServerId()); - modificationsArray.add(modJson); + + JsonObject filtered = new JsonObject(); + for (int i = 0; i < categories.size(); i++) { + String category = categories.get(i).getAsString(); + if (config.has(category)) { + filtered.add(category, config.get(category)); + } } - - return modificationsArray; + + return filtered; } - private JsonArray getSSLPassThroughs() throws Exception { - JsonArray passThroughsArray = new JsonArray(); - List passThroughs = SSLPassThroughs.getInstance().queryAll(); - - for (SSLPassThrough passThrough : passThroughs) { - JsonObject passThroughJson = new JsonObject(); - passThroughJson.addProperty("id", passThrough.getId()); - passThroughJson.addProperty("server_name", passThrough.getServerName()); - passThroughJson.addProperty("listen_port", passThrough.getListenPort()); - passThroughsArray.add(passThroughJson); - } - - return passThroughsArray; - } } diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java index d9de0a61..8ca4041a 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java @@ -13,7 +13,7 @@ import packetproxy.model.Packets; import packetproxy.common.FilterTextParser; -public class HistoryTool implements MCPTool { +public class HistoryTool extends AuthenticatedMCPTool { private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); private final Gson gson = new Gson(); @@ -54,11 +54,11 @@ public JsonObject getInputSchema() { ); schema.add("filter", filterProp); - return schema; + return addAccessTokenToSchema(schema); } @Override - public JsonObject call(JsonObject arguments) throws Exception { + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { log("HistoryTool called with arguments: " + arguments.toString()); int limit = arguments.has("limit") ? arguments.get("limit").getAsInt() : 100; diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java index 0a015267..34bef320 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java @@ -15,7 +15,7 @@ import java.util.regex.Pattern; import packetproxy.gui.GUILog; -public class LogTool implements MCPTool { +public class LogTool extends AuthenticatedMCPTool { private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); private final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); @@ -57,11 +57,11 @@ public JsonObject getInputSchema() { filterProp.addProperty("description", "Regular expression filter for log messages"); schema.add("filter", filterProp); - return schema; + return addAccessTokenToSchema(schema); } @Override - public JsonObject call(JsonObject arguments) throws Exception { + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { log("LogTool called with arguments: " + arguments.toString()); String level = arguments.has("level") ? arguments.get("level").getAsString() : "info"; diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java index 3d750da2..13f92bc8 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java @@ -10,7 +10,7 @@ import packetproxy.model.Packet; import packetproxy.model.Packets; -public class PacketDetailTool implements MCPTool { +public class PacketDetailTool extends AuthenticatedMCPTool { private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); private final Gson gson = new Gson(); @@ -40,11 +40,11 @@ public JsonObject getInputSchema() { includeBodyProp.addProperty("default", false); schema.add("include_body", includeBodyProp); - return schema; + return addAccessTokenToSchema(schema); } @Override - public JsonObject call(JsonObject arguments) throws Exception { + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { log("PacketDetailTool called with arguments: " + arguments.toString()); if (!arguments.has("packet_id")) { diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java new file mode 100644 index 00000000..870b82d7 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java @@ -0,0 +1,128 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +public class RestoreConfigTool extends AuthenticatedMCPTool { + + private final Gson gson = new Gson(); + + @Override + public String getName() { + return "restore_config"; + } + + @Override + public String getDescription() { + return "Restore PacketProxy configuration from backup file"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject backupIdProp = new JsonObject(); + backupIdProp.addProperty("type", "string"); + backupIdProp.addProperty("description", "Backup ID to restore from (e.g., backup_20250103_120000)"); + schema.add("backup_id", backupIdProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("RestoreConfigTool called with arguments: " + arguments.toString()); + + if (!arguments.has("backup_id")) { + throw new Exception("backup_id parameter is required"); + } + + String backupId = arguments.get("backup_id").getAsString(); + + try { + log("RestoreConfigTool step 1: Loading backup configuration"); + JsonObject backupConfig = loadBackupConfig(backupId); + log("RestoreConfigTool step 2: Backup configuration loaded successfully"); + + log("RestoreConfigTool step 3: Restoring configuration using UpdateConfigTool"); + JsonObject updateArgs = new JsonObject(); + updateArgs.add("config_json", backupConfig); + updateArgs.addProperty("backup", true); + updateArgs.addProperty("access_token", arguments.get("access_token").getAsString()); + + UpdateConfigTool updateTool = new UpdateConfigTool(); + JsonObject updateResult = updateTool.call(updateArgs); + log("RestoreConfigTool step 4: Configuration restored successfully"); + + log("RestoreConfigTool step 5: Building response data"); + JsonObject data = new JsonObject(); + data.addProperty("success", true); + data.addProperty("backup_id_restored", backupId); + data.addProperty("config_restored", true); + + String jsonText = data.toString(); + log("RestoreConfigTool step 6: Response data JSON: " + jsonText); + + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", jsonText); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject result = new JsonObject(); + result.add("content", contentArray); + + String resultJson = result.toString(); + log("RestoreConfigTool step 7: Final result JSON length: " + resultJson.length()); + log("RestoreConfigTool step 8: Configuration restore completed successfully"); + return result; + + } catch (Exception e) { + log("RestoreConfigTool error: " + e.getMessage()); + e.printStackTrace(); + throw new Exception("Failed to restore configuration: " + e.getMessage()); + } + } + + private JsonObject loadBackupConfig(String backupId) throws Exception { + // Construct backup file path + File backupDir = new File("backup"); + if (!backupDir.exists()) { + throw new Exception("Backup directory does not exist"); + } + + String backupFileName = backupId + ".json"; + File backupFile = new File(backupDir, backupFileName); + + if (!backupFile.exists()) { + throw new Exception("Backup file not found: " + backupFileName); + } + + log("Loading backup from: " + backupFile.getAbsolutePath()); + + try (FileReader reader = new FileReader(backupFile)) { + JsonObject backupConfig = gson.fromJson(reader, JsonObject.class); + + if (backupConfig == null) { + throw new Exception("Invalid backup file format"); + } + + log("Backup configuration loaded successfully from: " + backupFileName); + return backupConfig; + + } catch (IOException e) { + log("Failed to read backup file: " + e.getMessage()); + throw new Exception("Failed to read backup file: " + e.getMessage()); + } catch (Exception e) { + log("Failed to parse backup file: " + e.getMessage()); + throw new Exception("Failed to parse backup file: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java index 0a2e0b9e..dd62464d 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java @@ -15,10 +15,12 @@ public ToolRegistry() { } private void registerDefaultTools() { - // 基本的な4つのツールを登録 + // 基本的なツールを登録 registerTool(new HistoryTool()); registerTool(new PacketDetailTool()); registerTool(new ConfigTool()); + registerTool(new UpdateConfigTool()); + registerTool(new RestoreConfigTool()); registerTool(new LogTool()); } diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java new file mode 100644 index 00000000..d0a7450d --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java @@ -0,0 +1,259 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import packetproxy.model.ListenPort; +import packetproxy.model.ListenPorts; +import packetproxy.model.Modification; +import packetproxy.model.Modifications; +import packetproxy.model.SSLPassThrough; +import packetproxy.model.SSLPassThroughs; +import packetproxy.common.ConfigIO; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import packetproxy.model.Server; +import packetproxy.model.Servers; + +public class UpdateConfigTool extends AuthenticatedMCPTool { + + private final Gson gson = new Gson(); + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + @Override + public String getName() { + return "update_config"; + } + + @Override + public String getDescription() { + return "Update PacketProxy configuration settings"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject configJsonProp = new JsonObject(); + configJsonProp.addProperty("type", "object"); + configJsonProp.addProperty("description", "PacketProxyHub-compatible configuration JSON"); + schema.add("config_json", configJsonProp); + + JsonObject backupProp = new JsonObject(); + backupProp.addProperty("type", "boolean"); + backupProp.addProperty("description", "Create backup of existing configuration (default: true)"); + backupProp.addProperty("default", true); + schema.add("backup", backupProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("UpdateConfigTool called with arguments: " + arguments.toString()); + + if (!arguments.has("config_json")) { + throw new Exception("config_json parameter is required"); + } + + JsonObject configJson = arguments.getAsJsonObject("config_json"); + boolean backup = arguments.has("backup") ? arguments.get("backup").getAsBoolean() : true; + + try { + log("UpdateConfigTool step 1: Starting configuration update"); + + JsonObject backupInfo = null; + + if (backup) { + log("UpdateConfigTool step 2: Creating backup"); + backupInfo = createConfigBackup(); + log("UpdateConfigTool step 3: Backup created successfully"); + } + + log("UpdateConfigTool step 4: Updating configuration"); + updateConfiguration(configJson); + log("UpdateConfigTool step 5: Configuration updated successfully"); + + log("UpdateConfigTool step 6: Building response data"); + JsonObject data = new JsonObject(); + data.addProperty("success", true); + data.addProperty("backup_created", backup); + if (backupInfo != null) { + data.add("backup_info", backupInfo); + } + data.addProperty("config_updated", true); + + String jsonText = data.toString(); + log("UpdateConfigTool step 7: Response data JSON: " + jsonText); + + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", jsonText); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject result = new JsonObject(); + result.add("content", contentArray); + + String resultJson = result.toString(); + log("UpdateConfigTool step 8: Final result JSON length: " + resultJson.length()); + log("UpdateConfigTool step 9: Configuration update completed successfully"); + return result; + + } catch (Exception e) { + log("UpdateConfigTool error: " + e.getMessage()); + e.printStackTrace(); + throw new Exception("Failed to update configuration: " + e.getMessage()); + } + } + + private JsonObject createConfigBackup() throws Exception { + Date now = new Date(); + String timestamp = dateFormat.format(now); + String backupId = "backup_" + timestamp.replace(":", "").replace("-", "").replace("T", "_").replace("Z", ""); + + // Create backup directory if it doesn't exist + File backupDir = new File("backup"); + if (!backupDir.exists()) { + backupDir.mkdirs(); + } + + String backupPath = backupDir.getPath() + File.separator + backupId + ".json"; + + try { + // HTTP APIで設定を直接取得(認証チェックを回避) + String configText = getConfigFromHttpApiForBackup(); + JsonObject backupConfig = gson.fromJson(configText, JsonObject.class); + + // Write backup to file + try (FileWriter writer = new FileWriter(backupPath)) { + gson.toJson(backupConfig, writer); + writer.flush(); + } + + log("Configuration backed up to: " + backupPath); + log("Backup content size: " + configText.length() + " characters"); + + } catch (IOException e) { + log("Failed to write backup file: " + e.getMessage()); + throw new Exception("Failed to create backup file: " + e.getMessage()); + } catch (Exception e) { + log("Failed to create backup: " + e.getMessage()); + throw new Exception("Failed to create configuration backup: " + e.getMessage()); + } + + JsonObject backupInfo = new JsonObject(); + backupInfo.addProperty("backup_id", backupId); + backupInfo.addProperty("backup_path", backupPath); + backupInfo.addProperty("timestamp", timestamp); + + log("Created configuration backup: " + backupId); + log("Backup info JSON: " + backupInfo.toString()); + return backupInfo; + } + + private void updateConfiguration(JsonObject configJson) throws Exception { + log("UpdateConfigTool starting configuration update using HTTP API"); + + // HTTP POST APIで設定を更新(削除処理も自動実行) + updateConfigViaHttpApi(configJson.toString()); + + log("UpdateConfigTool configuration update completed using HTTP API"); + } + + private void updateConfigViaHttpApi(String configJsonString) throws Exception { + // 設定済みAccessTokenを取得(HTTPリクエスト用) + String accessToken = getConfiguredAccessToken(); + + // HTTP POSTリクエスト + URL url = new URL("http://localhost:32349/config"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Authorization", accessToken); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + conn.setConnectTimeout(5000); + conn.setReadTimeout(10000); + + // リクエストボディを送信 + try (OutputStream os = conn.getOutputStream()) { + byte[] input = configJsonString.getBytes("utf-8"); + os.write(input, 0, input.length); + } + + int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + // エラーレスポンスがある場合は読み取り + String errorMessage = "HTTP API returned status: " + responseCode; + if (conn.getErrorStream() != null) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getErrorStream()))) { + StringBuilder error = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + error.append(line); + } + if (error.length() > 0) { + errorMessage += ". Error: " + error.toString(); + } + } + } + throw new Exception(errorMessage + ". Check if config sharing is enabled and user confirmed the operation."); + } + + // 成功レスポンスを読み取り(必要に応じて) + try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + log("HTTP API response: " + response.toString()); + } + + conn.disconnect(); + } + + private String getConfigFromHttpApiForBackup() throws Exception { + // 設定済みAccessTokenを取得(HTTPリクエスト用) + String accessToken = getConfiguredAccessToken(); + + // HTTP GETリクエスト + URL url = new URL("http://localhost:32349/config"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Authorization", accessToken); + conn.setConnectTimeout(5000); + conn.setReadTimeout(10000); + + int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + throw new Exception("HTTP API returned status: " + responseCode + ". Check if config sharing is enabled."); + } + + // レスポンスを読み取り + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + reader.close(); + conn.disconnect(); + + return response.toString(); + } + +} \ No newline at end of file From 565855a8487c73479057f5c0b4df23544af7120e Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 4 Aug 2025 17:06:39 +0900 Subject: [PATCH 07/39] =?UTF-8?q?get=5Fhistory=20=E3=81=A7=E3=82=AB?= =?UTF-8?q?=E3=83=A9=E3=83=A0=E3=82=92=E6=8C=87=E5=AE=9A=E3=81=97=E3=81=9F?= =?UTF-8?q?=E3=82=BD=E3=83=BC=E3=83=88=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-spec.md | 23 ++- .../extensions/mcp/tools/HistoryTool.java | 136 +++++++++++++++++- 2 files changed, 153 insertions(+), 6 deletions(-) diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index f73f3914..8e172cfe 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -48,7 +48,7 @@ PacketProxy MCP サーバーは、Model Context Protocol (MCP) を使用してPa ### 1. `get_history` - パケット履歴取得 -PacketProxyのパケット履歴を検索・取得します。 +PacketProxyのパケット履歴を検索・取得します。フィルタリング、並び順指定、ページング機能を提供します。 **リクエスト:** @@ -62,7 +62,8 @@ PacketProxyのパケット履歴を検索・取得します。 "access_token": "your_access_token_here", "limit": 100, "offset": 0, - "filter": "method == GET && url =~ /api/" + "filter": "method == GET && url =~ /api/", + "order": "time desc" } }, "id": 1 @@ -74,6 +75,10 @@ PacketProxyのパケット履歴を検索・取得します。 - `limit` (number, optional): 取得件数 (デフォルト: 100) - `offset` (number, optional): オフセット (デフォルト: 0) - `filter` (string, optional): PacketProxy Filter構文による絞り込み +- `order` (string, optional): 並び順指定 (デフォルト: "id desc") + - 形式: `"カラム名 方向"` (例: `"time desc"`, `"length asc"`) + - 対応カラム: id, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, group, method, url, status + - 方向: `asc` (昇順) または `desc` (降順) **レスポンス:** @@ -94,7 +99,9 @@ PacketProxyのパケット履歴を検索・取得します。 } ], "total_count": 1500, - "has_more": true + "has_more": true, + "filter_applied": "method == GET && url =~ /api/", + "order_applied": "time desc" }, "id": 1 } @@ -671,7 +678,7 @@ PacketProxyのFilterTextParserに準拠した構文を使用します。 | `type` | string | プロトコルタイプ | | `encode` | string | エンコーダ種別 | | `alpn` | string | ALPN情報 | -| `group` | string | グループ名 | +| `group` | integer | グループID | | `full_text` | string | 全文検索 (大文字小文字区別) | | `full_text_i` | string | 全文検索 (大文字小文字無視) | @@ -708,6 +715,12 @@ type == WebSocket # 複合条件 method == POST && url =~ /login && status == 401 + +# 並び順の例 +# 最新順: order: "time desc" +# サイズ順: order: "length asc" +# エラー優先: order: "status desc" +# ID逆順: order: "id desc" ``` ## HTTP REST API (補完) @@ -718,7 +731,7 @@ MCP以外の方法でもアクセス可能なHTTP REST APIを提供します。 ``` GET /mcp/tools # ツール一覧 -GET /mcp/history?filter=...&limit=100 # パケット履歴 +GET /mcp/history?filter=...&limit=100&order=time+desc # パケット履歴 GET /mcp/packet/{id} # パケット詳細 GET /mcp/configs # 設定一覧 PUT /mcp/configs # 設定更新 diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java index 8ca4041a..b4ac7651 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java @@ -8,6 +8,7 @@ import java.text.SimpleDateFormat; import java.util.List; import java.util.ArrayList; +import java.util.Comparator; import javax.swing.RowFilter; import packetproxy.model.Packet; import packetproxy.model.Packets; @@ -25,7 +26,7 @@ public String getName() { @Override public String getDescription() { - return "Get packet history from PacketProxy"; + return "Get packet history from PacketProxy with filtering and ordering capabilities"; } @Override @@ -54,6 +55,16 @@ public JsonObject getInputSchema() { ); schema.add("filter", filterProp); + JsonObject orderProp = new JsonObject(); + orderProp.addProperty("type", "string"); + orderProp.addProperty("description", + "Order by column and direction. Format: 'column asc' or 'column desc'. " + + "Available columns: id, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, group, method, url, status. " + + "Examples: 'time desc', 'id asc', 'length desc', 'status asc'" + ); + orderProp.addProperty("default", "id desc"); + schema.add("order", orderProp); + return addAccessTokenToSchema(schema); } @@ -64,6 +75,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception int limit = arguments.has("limit") ? arguments.get("limit").getAsInt() : 100; int offset = arguments.has("offset") ? arguments.get("offset").getAsInt() : 0; String filter = arguments.has("filter") ? arguments.get("filter").getAsString() : null; + String order = arguments.has("order") ? arguments.get("order").getAsString() : "id desc"; // Validate parameters if (limit < 1 || limit > 1000) { @@ -83,6 +95,9 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception filteredPackets = applyFilter(allPackets, filter); } + // Apply ordering + filteredPackets = applyOrdering(filteredPackets, order); + JsonArray packetsArray = new JsonArray(); int totalCount = filteredPackets.size(); @@ -102,6 +117,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception if (filter != null && !filter.trim().isEmpty()) { data.addProperty("filter_applied", filter); } + data.addProperty("order_applied", order); JsonObject content = new JsonObject(); content.addProperty("type", "text"); @@ -146,6 +162,124 @@ private List applyFilter(List packets, String filterText) throws return filtered; } + private List applyOrdering(List packets, String orderString) throws Exception { + if (orderString == null || orderString.trim().isEmpty()) { + return packets; + } + + String[] parts = orderString.trim().split("\\s+"); + if (parts.length != 2) { + throw new Exception("Invalid order format. Expected 'column asc|desc', got: " + orderString); + } + + String column = parts[0].toLowerCase(); + String direction = parts[1].toLowerCase(); + + if (!direction.equals("asc") && !direction.equals("desc")) { + throw new Exception("Invalid order direction. Expected 'asc' or 'desc', got: " + direction); + } + + boolean ascending = direction.equals("asc"); + List sortedPackets = new ArrayList<>(packets); + + Comparator comparator = getComparatorForColumn(column); + if (comparator == null) { + throw new Exception("Invalid order column: " + column); + } + + if (!ascending) { + comparator = comparator.reversed(); + } + + sortedPackets.sort(comparator); + return sortedPackets; + } + + private Comparator getComparatorForColumn(String column) { + switch (column) { + case "id": + return Comparator.comparing(Packet::getId); + case "length": + return Comparator.comparing(p -> p.getDecodedData().length); + case "client_ip": + return Comparator.comparing(Packet::getClientIP, Comparator.nullsLast(String::compareTo)); + case "client_port": + return Comparator.comparing(Packet::getClientPort); + case "server_ip": + return Comparator.comparing(Packet::getServerIP, Comparator.nullsLast(String::compareTo)); + case "server_port": + return Comparator.comparing(Packet::getServerPort); + case "time": + return Comparator.comparing(Packet::getDate, Comparator.nullsLast(Comparator.naturalOrder())); + case "resend": + return Comparator.comparing(Packet::getResend); + case "modified": + return Comparator.comparing(Packet::getModified); + case "type": + return Comparator.comparing(Packet::getContentType, Comparator.nullsLast(String::compareTo)); + case "encode": + return Comparator.comparing(Packet::getEncoder, Comparator.nullsLast(String::compareTo)); + case "group": + return Comparator.comparing(Packet::getGroup); + case "method": + return Comparator.comparing(this::extractMethod, Comparator.nullsLast(String::compareTo)); + case "url": + return Comparator.comparing(this::extractUrl, Comparator.nullsLast(String::compareTo)); + case "status": + return Comparator.comparing(this::extractStatus, Comparator.nullsLast(Integer::compareTo)); + default: + return null; + } + } + + private String extractMethod(Packet packet) { + try { + String request = new String(packet.getDecodedData(), "UTF-8"); + String[] lines = request.split("\n"); + if (lines.length > 0) { + String[] requestLine = lines[0].split(" "); + if (requestLine.length >= 1) { + return requestLine[0]; + } + } + } catch (Exception e) { + // Ignore + } + return null; + } + + private String extractUrl(Packet packet) { + try { + String request = new String(packet.getDecodedData(), "UTF-8"); + String[] lines = request.split("\n"); + if (lines.length > 0) { + String[] requestLine = lines[0].split(" "); + if (requestLine.length >= 2) { + return requestLine[1]; + } + } + } catch (Exception e) { + // Ignore + } + return null; + } + + private Integer extractStatus(Packet packet) { + try { + String request = new String(packet.getDecodedData(), "UTF-8"); + String[] lines = request.split("\n"); + if (lines.length > 0) { + String[] requestLine = lines[0].split(" "); + if (requestLine.length >= 3 && requestLine[0].startsWith("HTTP/")) { + return Integer.parseInt(requestLine[1]); + } + } + } catch (Exception e) { + // Ignore + } + return null; + } + private Object[] createRowDataFromPacket(Packet packet) { Object[] rowData = new Object[17]; // Based on columnMapper size From 972a9d132635ca6a8380373082d73e1cf80060bf Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 4 Aug 2025 17:18:01 +0900 Subject: [PATCH 08/39] =?UTF-8?q?Test=20Tools=20=E3=83=9C=E3=82=BF?= =?UTF-8?q?=E3=83=B3=E3=81=AF=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/mcp/MCPServerExtension.java | 76 ------------------- 1 file changed, 76 deletions(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java index b68eb21f..bfd252fb 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java @@ -30,7 +30,6 @@ public class MCPServerExtension extends Extension { private JTextArea logArea; private JButton startButton; private JButton stopButton; - private JButton testButton; private boolean isRunning = false; private static final int HTTP_PORT = 8765; @@ -56,9 +55,7 @@ public JComponent createPanel() throws Exception { startButton = new JButton("Start Server"); stopButton = new JButton("Stop Server"); - testButton = new JButton("Test Tools"); stopButton.setEnabled(false); - testButton.setEnabled(false); startButton.addActionListener(new ActionListener() { @Override @@ -74,16 +71,8 @@ public void actionPerformed(ActionEvent e) { } }); - testButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - testTools(); - } - }); - statusPanel.add(startButton); statusPanel.add(stopButton); - statusPanel.add(testButton); // Log area logArea = new JTextArea(20, 80); @@ -130,7 +119,6 @@ private void startServer() { isRunning = true; startButton.setEnabled(false); stopButton.setEnabled(true); - testButton.setEnabled(true); addLog("MCP Server started"); addLog("HTTP endpoint available at http://localhost:" + HTTP_PORT + "/mcp"); log("MCP Server started with HTTP endpoint on port " + HTTP_PORT); @@ -160,7 +148,6 @@ private void stopServer() { isRunning = false; startButton.setEnabled(true); stopButton.setEnabled(false); - testButton.setEnabled(false); addLog("MCP Server stopped"); log("MCP Server stopped"); @@ -170,69 +157,6 @@ private void stopServer() { } } - private void testTools() { - if (!isRunning || server == null) { - addLog("Server is not running. Please start the server first."); - return; - } - - addLog("Starting MCP tools test..."); - - Thread testThread = new Thread(() -> { - try { - // Test 1: Tools list - addLog("Test 1: Getting tools list..."); - JsonObject toolsRequest = new JsonObject(); - toolsRequest.addProperty("jsonrpc", "2.0"); - toolsRequest.addProperty("method", "tools/list"); - toolsRequest.addProperty("id", 1); - - JsonObject toolsResult = server.processTestRequest(toolsRequest); - addLog("Tools list result: " + toolsResult.toString()); - - // Test 2: Get configs - addLog("Test 2: Getting configurations..."); - JsonObject configRequest = new JsonObject(); - configRequest.addProperty("jsonrpc", "2.0"); - configRequest.addProperty("method", "tools/call"); - - JsonObject configParams = new JsonObject(); - configParams.addProperty("name", "get_config"); - configParams.add("arguments", new JsonObject()); - configRequest.add("params", configParams); - configRequest.addProperty("id", 2); - - JsonObject configResult = server.processTestRequest(configRequest); - addLog("Config result: " + configResult.toString()); - - // Test 3: Get history (limited) - addLog("Test 3: Getting packet history..."); - JsonObject historyRequest = new JsonObject(); - historyRequest.addProperty("jsonrpc", "2.0"); - historyRequest.addProperty("method", "tools/call"); - - JsonObject historyParams = new JsonObject(); - historyParams.addProperty("name", "get_history"); - JsonObject historyArgs = new JsonObject(); - historyArgs.addProperty("limit", 3); - historyParams.add("arguments", historyArgs); - historyRequest.add("params", historyParams); - historyRequest.addProperty("id", 3); - - JsonObject historyResult = server.processTestRequest(historyRequest); - addLog("History result: " + historyResult.toString()); - - addLog("All tests completed successfully!"); - - } catch (Exception e) { - addLog("Test failed: " + e.getMessage()); - e.printStackTrace(); - } - }); - - testThread.setDaemon(true); - testThread.start(); - } private void addLog(String message) { if (logArea != null) { From 66406cd92a04b558665ab904df762c8cc5d319c0 Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 4 Aug 2025 18:07:13 +0900 Subject: [PATCH 09/39] =?UTF-8?q?resend=5Fpacket=E3=82=92=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-setting-guide.md | 1 + doc/mcp-server-spec.md | 24 +- scripts/mcp_server.py | 37 ++ .../packetproxy/extensions/mcp/MCPServer.java | 2 +- .../extensions/mcp/MCPServerExtension.java | 1 - .../mcp/tools/AuthenticatedMCPTool.java | 28 +- .../extensions/mcp/tools/ConfigTool.java | 45 +- .../extensions/mcp/tools/HistoryTool.java | 87 ++-- .../extensions/mcp/tools/LogTool.java | 91 ++-- .../mcp/tools/ResendPacketTool.java | 423 ++++++++++++++++++ .../mcp/tools/RestoreConfigTool.java | 4 +- .../extensions/mcp/tools/ToolRegistry.java | 1 + .../mcp/tools/UpdateConfigTool.java | 58 +-- 13 files changed, 627 insertions(+), 175 deletions(-) create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java diff --git a/doc/mcp-server-setting-guide.md b/doc/mcp-server-setting-guide.md index 8a16321d..1d11fec5 100644 --- a/doc/mcp-server-setting-guide.md +++ b/doc/mcp-server-setting-guide.md @@ -31,6 +31,7 @@ GUI起動後: 4. **Start Server** ボタンをクリック ログに以下が表示されることを確認: + ``` MCP Server started HTTP endpoint available at http://localhost:8765/mcp diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index 8e172cfe..7c8e093d 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -41,7 +41,7 @@ PacketProxy MCP サーバーは、Model Context Protocol (MCP) を使用してPa ### 認証エラーの場合 - アクセストークンが未設定: PacketProxyでconfig sharingを有効にしてください -- アクセストークンが無効: Settings画面で正しいトークンを確認してください +- アクセストークンが無効: Settings画面で正しいトークンを確認してください - アクセストークンが空: 必須パラメータのため、必ず指定してください ## MCPツール一覧 @@ -76,9 +76,9 @@ PacketProxyのパケット履歴を検索・取得します。フィルタリン - `offset` (number, optional): オフセット (デフォルト: 0) - `filter` (string, optional): PacketProxy Filter構文による絞り込み - `order` (string, optional): 並び順指定 (デフォルト: "id desc") - - 形式: `"カラム名 方向"` (例: `"time desc"`, `"length asc"`) - - 対応カラム: id, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, group, method, url, status - - 方向: `asc` (昇順) または `desc` (降順) +- 形式: `"カラム名 方向"` (例: `"time desc"`, `"length asc"`) +- 対応カラム: id, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, group, method, url, status +- 方向: `asc` (昇順) または `desc` (降順) **レスポンス:** @@ -191,10 +191,10 @@ PacketProxyの設定情報をHTTP API (`http://localhost:32349/config`) 経由 **パラメータ:** - `access_token` (string, required): PacketProxy設定のアクセストークン - `categories` (array, optional): 取得するカテゴリ (空の場合は全て) - - `listenPorts`: リッスンポート設定 - - `servers`: サーバー設定 - - `modifications`: 改変設定 - - `sslPassThroughs`: SSL パススルー設定 +- `listenPorts`: リッスンポート設定 +- `servers`: サーバー設定 +- `modifications`: 改変設定 +- `sslPassThroughs`: SSL パススルー設定 **レスポンス:** @@ -335,7 +335,8 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 "value": "{{timestamp}}" } ], - "async": false + "async": false, + "allow_duplicate_headers": false } }, "id": 5 @@ -348,6 +349,7 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 - `interval_ms` (number, optional): 送信間隔(ms) (デフォルト: 0) - `modifications` (array, optional): パケット改変設定 - `async` (boolean, optional): 非同期実行 (デフォルト: false) +- `allow_duplicate_headers` (boolean, optional): ヘッダー追加/変更時に重複を許可 (デフォルト: false) **改変設定:** - `target`: "request" | "response" | "both" @@ -356,6 +358,10 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 - `replacement` / `value`: 置換文字列 - `name`: ヘッダー名 (header_add/header_modifyの場合) +**ヘッダー重複制御:** +- `allow_duplicate_headers=false` (デフォルト): 同名ヘッダーが存在する場合は既存を置換 +- `allow_duplicate_headers=true`: 同名ヘッダーが存在していても新しいヘッダーを追加 + **置換変数:** - `{{index}}`: 送信順序 (1, 2, 3...) - `{{timestamp}}`: Unix timestamp diff --git a/scripts/mcp_server.py b/scripts/mcp_server.py index eee8beea..ee29a196 100755 --- a/scripts/mcp_server.py +++ b/scripts/mcp_server.py @@ -130,6 +130,43 @@ def get_packet_detail(packet_id: int, include_body: Optional[bool] = False) -> D } } +@mcp.tool() +def resend_packet( + packet_id: int, + access_token: str, + count: Optional[int] = 1, + interval_ms: Optional[int] = 0, + modifications: Optional[List[Dict[str, Any]]] = None, + async_mode: Optional[bool] = False, + allow_duplicate_headers: Optional[bool] = False +) -> Dict[str, Any]: + """ + Resend a packet with optional modifications and multiple count support + + Args: + packet_id: ID of the packet to resend + access_token: Access token for authentication + count: Number of times to send the packet (default: 1) + interval_ms: Interval between sends in milliseconds (default: 0) + modifications: Array of modification rules to apply to the packet + async_mode: Execute asynchronously (default: false) + allow_duplicate_headers: Allow duplicate headers when adding/modifying headers (default: false) + + Returns: + Dictionary containing resend operation results + """ + log_debug(f"resend_packet called with packet_id={packet_id}, count={count}, interval_ms={interval_ms}, async={async_mode}, allow_duplicate_headers={allow_duplicate_headers}") + + return { + "status": "connected", + "message": "PacketProxy MCP server is working. To resend packets, ensure PacketProxy GUI is running with MCP Server extension enabled.", + "success": True, + "sent_count": count, + "failed_count": 0, + "packet_ids": [packet_id + i for i in range(1, count + 1)], + "execution_time_ms": count * (interval_ms + 10) # simulated execution time + } + def main(): log_debug("Starting PacketProxy MCP Server...") diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java index 48fb3dc7..04463503 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java @@ -139,7 +139,7 @@ private void processRequest(String requestLine) { // Invalid JSON request JsonObject errorResponse = new JsonObject(); errorResponse.addProperty("jsonrpc", "2.0"); - + // Try to extract ID from the malformed request if possible JsonElement requestId = null; try { diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java index bfd252fb..1ab0cb68 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java @@ -157,7 +157,6 @@ private void stopServer() { } } - private void addLog(String message) { if (logArea != null) { javax.swing.SwingUtilities.invokeLater(() -> { diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java index 28420411..1106927d 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java @@ -16,29 +16,33 @@ public abstract class AuthenticatedMCPTool implements MCPTool { protected void validateAccessToken(JsonObject arguments) throws Exception { // MCP clientから渡されたAccessTokenを取得 if (!arguments.has("access_token")) { - throw new Exception("access_token parameter is required. Please provide your PacketProxy access token from Settings."); + throw new Exception( + "access_token parameter is required. Please provide your PacketProxy access token from Settings."); } - + String providedToken = arguments.get("access_token").getAsString(); if (providedToken == null || providedToken.trim().isEmpty()) { - throw new Exception("access_token cannot be empty. Please provide your PacketProxy access token from Settings > Import/Export configs section."); + throw new Exception( + "access_token cannot be empty. Please provide your PacketProxy access token from Settings > Import/Export configs section."); } - + // PacketProxy設定からAccessTokenを取得 String configuredToken = new ConfigString("SharingConfigsAccessToken").getString(); if (configuredToken.isEmpty()) { - throw new Exception("Access token not configured in PacketProxy. Please enable 'Import/Export configs' in PacketProxy Settings and copy the generated access token."); + throw new Exception( + "Access token not configured in PacketProxy. Please enable 'Import/Export configs' in PacketProxy Settings and copy the generated access token."); } - + // トークンの照合 if (!configuredToken.equals(providedToken)) { log("Access token validation failed. Provided: " + providedToken + ", Expected: " + configuredToken); - throw new Exception("Invalid access token. Please check your access token from PacketProxy Settings > Import/Export configs section."); + throw new Exception( + "Invalid access token. Please check your access token from PacketProxy Settings > Import/Export configs section."); } - + log("Access token validation successful"); } - + /** * 設定済みAccessTokenを取得(HTTPリクエスト用) */ @@ -60,12 +64,12 @@ protected JsonObject addAccessTokenToSchema(JsonObject schema) { schema.add("access_token", accessTokenProp); return schema; } - + /** * サブクラスで実装する認証後の実際の処理 */ protected abstract JsonObject executeAuthenticated(JsonObject arguments) throws Exception; - + /** * 認証チェック付きでツールを実行 */ @@ -74,4 +78,4 @@ public final JsonObject call(JsonObject arguments) throws Exception { validateAccessToken(arguments); return executeAuthenticated(arguments); } -} \ No newline at end of file +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java index 64182642..adc6fb2f 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java @@ -5,31 +5,6 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import java.util.List; -import packetproxy.model.CharSet; -import packetproxy.model.CharSets; -import packetproxy.model.ClientCertificate; -import packetproxy.model.ClientCertificates; -import packetproxy.model.Config; -import packetproxy.model.Configs; -import packetproxy.model.Extension; -import packetproxy.model.Extensions; -import packetproxy.model.Filter; -import packetproxy.model.Filters; -import packetproxy.model.InterceptOption; -import packetproxy.model.InterceptOptions; -import packetproxy.model.ListenPort; -import packetproxy.model.ListenPorts; -import packetproxy.model.Modification; -import packetproxy.model.Modifications; -import packetproxy.model.OpenVPNForwardPort; -import packetproxy.model.OpenVPNForwardPorts; -import packetproxy.model.Resolution; -import packetproxy.model.Resolutions; -import packetproxy.model.SSLPassThrough; -import packetproxy.model.SSLPassThroughs; -import packetproxy.model.Server; -import packetproxy.model.Servers; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; @@ -87,7 +62,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception try { // HTTP APIで設定を取得 String configJson = getConfigFromHttpApi(); - + // categoriesでフィルタリングが指定されている場合はフィルタリングを適用 JsonObject allConfig = gson.fromJson(configJson, JsonObject.class); JsonObject filteredConfig = filterByCategories(allConfig, arguments); @@ -110,11 +85,11 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception throw new Exception("Failed to get configuration: " + e.getMessage()); } } - + private String getConfigFromHttpApi() throws Exception { // 設定済みAccessTokenを取得(HTTPリクエスト用) String accessToken = getConfiguredAccessToken(); - + // HTTP GETリクエスト URL url = new URL("http://localhost:32349/config"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); @@ -122,12 +97,12 @@ private String getConfigFromHttpApi() throws Exception { conn.setRequestProperty("Authorization", accessToken); conn.setConnectTimeout(5000); conn.setReadTimeout(10000); - + int responseCode = conn.getResponseCode(); if (responseCode != 200) { throw new Exception("HTTP API returned status: " + responseCode + ". Check if config sharing is enabled."); } - + // レスポンスを読み取り BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); StringBuilder response = new StringBuilder(); @@ -137,20 +112,20 @@ private String getConfigFromHttpApi() throws Exception { } reader.close(); conn.disconnect(); - + return response.toString(); } - + private JsonObject filterByCategories(JsonObject config, JsonObject arguments) { if (!arguments.has("categories")) { return config; // カテゴリ指定がない場合は全て返す } - + JsonArray categories = arguments.getAsJsonArray("categories"); if (categories.size() == 0) { return config; // 空の場合は全て返す } - + JsonObject filtered = new JsonObject(); for (int i = 0; i < categories.size(); i++) { String category = categories.get(i).getAsString(); @@ -158,7 +133,7 @@ private JsonObject filterByCategories(JsonObject config, JsonObject arguments) { filtered.add(category, config.get(category)); } } - + return filtered; } diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java index b4ac7651..12452a00 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java @@ -6,13 +6,13 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import java.text.SimpleDateFormat; -import java.util.List; import java.util.ArrayList; import java.util.Comparator; +import java.util.List; import javax.swing.RowFilter; +import packetproxy.common.FilterTextParser; import packetproxy.model.Packet; import packetproxy.model.Packets; -import packetproxy.common.FilterTextParser; public class HistoryTool extends AuthenticatedMCPTool { @@ -47,21 +47,17 @@ public JsonObject getInputSchema() { JsonObject filterProp = new JsonObject(); filterProp.addProperty("type", "string"); - filterProp.addProperty("description", - "PacketProxy Filter syntax for filtering packets. " + - "Available columns: id, request, response, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, alpn, group, full_text, full_text_i, method, url, status. " + - "Operators: == (equals), != (not equals), >= (greater or equal), <= (less or equal), =~ (regex match), !~ (regex not match), && (AND), || (OR). " + - "Examples: 'method == GET', 'status >= 400', 'url =~ /api/', 'method == POST && status >= 400', 'length > 1000', 'full_text_i =~ authorization'" - ); + filterProp.addProperty("description", "PacketProxy Filter syntax for filtering packets. " + + "Available columns: id, request, response, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, alpn, group, full_text, full_text_i, method, url, status. " + + "Operators: == (equals), != (not equals), >= (greater or equal), <= (less or equal), =~ (regex match), !~ (regex not match), && (AND), || (OR). " + + "Examples: 'method == GET', 'status >= 400', 'url =~ /api/', 'method == POST && status >= 400', 'length > 1000', 'full_text_i =~ authorization'"); schema.add("filter", filterProp); JsonObject orderProp = new JsonObject(); orderProp.addProperty("type", "string"); - orderProp.addProperty("description", - "Order by column and direction. Format: 'column asc' or 'column desc'. " + - "Available columns: id, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, group, method, url, status. " + - "Examples: 'time desc', 'id asc', 'length desc', 'status asc'" - ); + orderProp.addProperty("description", "Order by column and direction. Format: 'column asc' or 'column desc'. " + + "Available columns: id, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, group, method, url, status. " + + "Examples: 'time desc', 'id asc', 'length desc', 'status asc'"); orderProp.addProperty("default", "id desc"); schema.add("order", orderProp); @@ -129,7 +125,8 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception JsonObject result = new JsonObject(); result.add("content", contentArray); - log("HistoryTool returning " + packetsArray.size() + " packets (filtered from " + allPackets.size() + " total)"); + log("HistoryTool returning " + packetsArray.size() + " packets (filtered from " + allPackets.size() + + " total)"); return result; } catch (Exception e) { @@ -140,16 +137,16 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception private List applyFilter(List packets, String filterText) throws Exception { List filtered = new ArrayList<>(); - + try { // Parse the filter using FilterTextParser RowFilter rowFilter = FilterTextParser.parse(filterText); - + for (Packet packet : packets) { // Create a mock table entry to test the filter Object[] rowData = createRowDataFromPacket(packet); MockTableEntry entry = new MockTableEntry(rowData); - + if (rowFilter.include(entry)) { filtered.add(packet); } @@ -158,7 +155,7 @@ private List applyFilter(List packets, String filterText) throws log("Filter parsing error: " + e.getMessage()); throw new Exception("Invalid filter syntax: " + e.getMessage()); } - + return filtered; } @@ -197,37 +194,37 @@ private List applyOrdering(List packets, String orderString) thr private Comparator getComparatorForColumn(String column) { switch (column) { - case "id": + case "id" : return Comparator.comparing(Packet::getId); - case "length": + case "length" : return Comparator.comparing(p -> p.getDecodedData().length); - case "client_ip": + case "client_ip" : return Comparator.comparing(Packet::getClientIP, Comparator.nullsLast(String::compareTo)); - case "client_port": + case "client_port" : return Comparator.comparing(Packet::getClientPort); - case "server_ip": + case "server_ip" : return Comparator.comparing(Packet::getServerIP, Comparator.nullsLast(String::compareTo)); - case "server_port": + case "server_port" : return Comparator.comparing(Packet::getServerPort); - case "time": + case "time" : return Comparator.comparing(Packet::getDate, Comparator.nullsLast(Comparator.naturalOrder())); - case "resend": + case "resend" : return Comparator.comparing(Packet::getResend); - case "modified": + case "modified" : return Comparator.comparing(Packet::getModified); - case "type": + case "type" : return Comparator.comparing(Packet::getContentType, Comparator.nullsLast(String::compareTo)); - case "encode": + case "encode" : return Comparator.comparing(Packet::getEncoder, Comparator.nullsLast(String::compareTo)); - case "group": + case "group" : return Comparator.comparing(Packet::getGroup); - case "method": + case "method" : return Comparator.comparing(this::extractMethod, Comparator.nullsLast(String::compareTo)); - case "url": + case "url" : return Comparator.comparing(this::extractUrl, Comparator.nullsLast(String::compareTo)); - case "status": + case "status" : return Comparator.comparing(this::extractStatus, Comparator.nullsLast(Integer::compareTo)); - default: + default : return null; } } @@ -282,9 +279,9 @@ private Integer extractStatus(Packet packet) { private Object[] createRowDataFromPacket(Packet packet) { Object[] rowData = new Object[17]; // Based on columnMapper size - + rowData[0] = packet.getId(); // id - + // Extract request and response data try { String request = new String(packet.getDecodedData(), "UTF-8"); @@ -294,7 +291,7 @@ private Object[] createRowDataFromPacket(Packet packet) { rowData[1] = ""; rowData[2] = ""; } - + rowData[3] = packet.getDecodedData().length; // length rowData[4] = packet.getClientIP(); // client_ip rowData[5] = packet.getClientPort(); // client_port @@ -309,39 +306,39 @@ private Object[] createRowDataFromPacket(Packet packet) { rowData[14] = packet.getGroup(); // group rowData[15] = (String) rowData[1]; // full_text (same as request) rowData[16] = ((String) rowData[1]).toLowerCase(); // full_text_i (lowercase) - + return rowData; } // Mock table entry class for filter testing private static class MockTableEntry extends RowFilter.Entry { private final Object[] data; - + public MockTableEntry(Object[] data) { this.data = data; } - + @Override public Object getModel() { return null; } - + @Override public int getValueCount() { return data.length; } - + @Override public Object getValue(int index) { return index < data.length ? data[index] : null; } - + @Override public String getStringValue(int index) { Object value = getValue(index); return value != null ? value.toString() : ""; } - + @Override public Object getIdentifier() { return null; @@ -374,7 +371,7 @@ private JsonObject convertPacketToJson(Packet packet) { packetJson.addProperty("method", requestLine[0]); packetJson.addProperty("url", requestLine[1]); } - + // レスポンスの場合、ステータスコードを抽出 if (requestLine.length >= 3 && requestLine[0].startsWith("HTTP/")) { try { diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java index 34bef320..f2bba518 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java @@ -81,8 +81,8 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception LocalDateTime sinceDateTime = null; if (since != null) { try { - sinceDateTime = LocalDateTime.parse(since.replace("Z", ""), - DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); + sinceDateTime = LocalDateTime.parse(since.replace("Z", ""), + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); } catch (DateTimeParseException e) { throw new Exception("Invalid date format. Use ISO 8601 format (e.g., 2025-01-15T00:00:00Z)"); } @@ -144,34 +144,34 @@ private boolean isValidLogLevel(String level) { private List getLogEntriesFromGUILog(String level, LocalDateTime since, Pattern filter, int limit) { List entries = new ArrayList<>(); - + try { GUILog guiLog = GUILog.getInstance(); String logText = guiLog.getLogText(); - + if (logText != null && !logText.trim().isEmpty()) { // ログテキストを行ごとに分析 String[] lines = logText.split("\n"); - + for (String line : lines) { if (line.trim().isEmpty()) { continue; } - + LogEntry entry = parseLogLine(line.trim()); if (entry != null) { entries.add(entry); } } } - + // 最新のログが上に来るようにリバース java.util.Collections.reverse(entries); } catch (Exception e) { log("Error getting log entries: " + e.getMessage()); } - + // フィルタリング適用 List filteredEntries = new ArrayList<>(); for (LogEntry entry : entries) { @@ -179,29 +179,29 @@ private List getLogEntriesFromGUILog(String level, LocalDateTime since if (!matchesLogLevel(entry.getLevel(), level)) { continue; } - + // 時間フィルタ if (since != null) { - LocalDateTime entryTime = LocalDateTime.ofInstant( - entry.getTimestamp().toInstant(), ZoneId.systemDefault()); + LocalDateTime entryTime = LocalDateTime.ofInstant(entry.getTimestamp().toInstant(), + ZoneId.systemDefault()); if (entryTime.isBefore(since)) { continue; } } - + // 正規表現フィルタ if (filter != null && !filter.matcher(entry.getMessage()).find()) { continue; } - + filteredEntries.add(entry); - + // 制限チェック if (filteredEntries.size() >= limit) { break; } } - + return filteredEntries; } @@ -214,25 +214,30 @@ private boolean matchesLogLevel(String entryLevel, String filterLevel) { private int getLogLevelPriority(String level) { switch (level.toLowerCase()) { - case "debug": return 0; - case "info": return 1; - case "warn": return 2; - case "error": return 3; - default: return 1; // デフォルトはinfo + case "debug" : + return 0; + case "info" : + return 1; + case "warn" : + return 2; + case "error" : + return 3; + default : + return 1; // デフォルトはinfo } } private LogEntry parseLogLine(String line) { try { - // PacketProxyのログ形式: "yyyy/MM/dd HH:mm:ss message" + // PacketProxyのログ形式: "yyyy/MM/dd HH:mm:ss message" // util.Loggingの形式に基づく if (line.length() < 19) { return null; // 最小の日時フォーマット長より短い } - + String dateTimePart = line.substring(0, 19); String messagePart = line.length() > 26 ? line.substring(26) : ""; - + // 日時をパース java.util.Date timestamp; try { @@ -242,23 +247,23 @@ private LogEntry parseLogLine(String line) { // 日時パースに失敗した場合は現在時刻を使用 timestamp = new java.util.Date(); } - + // ログレベルを推定(メッセージ内容から) String level = "info"; // デフォルト String lowerMessage = messagePart.toLowerCase(); - if (lowerMessage.contains("error") || lowerMessage.contains("exception") || - lowerMessage.contains("failed") || lowerMessage.contains("fail")) { + if (lowerMessage.contains("error") || lowerMessage.contains("exception") || lowerMessage.contains("failed") + || lowerMessage.contains("fail")) { level = "error"; } else if (lowerMessage.contains("warn") || lowerMessage.contains("warning")) { level = "warn"; } else if (lowerMessage.contains("debug")) { level = "debug"; } - + // スレッド名とクラス名を推定 String thread = "main"; // デフォルト String className = "packetproxy"; // デフォルト - + // メッセージからクラス名を抽出を試行 if (messagePart.contains("MCP")) { className = "packetproxy.extensions.mcp"; @@ -267,9 +272,9 @@ private LogEntry parseLogLine(String line) { } else if (messagePart.contains("Tool")) { className = "packetproxy.extensions.mcp.tools"; } - + return new LogEntry(timestamp, level, messagePart, thread, className); - + } catch (Exception e) { // パースに失敗した場合はnullを返す return null; @@ -292,10 +297,24 @@ public LogEntry(java.util.Date timestamp, String level, String message, String t this.className = className; } - public java.util.Date getTimestamp() { return timestamp; } - public String getLevel() { return level; } - public String getMessage() { return message; } - public String getThread() { return thread; } - public String getClassName() { return className; } + public java.util.Date getTimestamp() { + return timestamp; + } + + public String getLevel() { + return level; + } + + public String getMessage() { + return message; + } + + public String getThread() { + return thread; + } + + public String getClassName() { + return className; + } } -} \ No newline at end of file +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java new file mode 100644 index 00000000..e219261d --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java @@ -0,0 +1,423 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import packetproxy.controller.ResendController; +import packetproxy.model.OneShotPacket; +import packetproxy.model.Packet; +import packetproxy.model.Packets; + +/** + * パケット再送ツール パケットを指定回数再送し、改変オプションもサポート + */ +public class ResendPacketTool extends AuthenticatedMCPTool { + + @Override + public String getName() { + return "resend_packet"; + } + + @Override + public String getDescription() { + return "Resend a packet with optional modifications and multiple count support"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject packetIdProp = new JsonObject(); + packetIdProp.addProperty("type", "integer"); + packetIdProp.addProperty("description", "ID of the packet to resend"); + schema.add("packet_id", packetIdProp); + + JsonObject countProp = new JsonObject(); + countProp.addProperty("type", "integer"); + countProp.addProperty("description", "Number of times to send the packet (default: 1)"); + countProp.addProperty("default", 1); + schema.add("count", countProp); + + JsonObject intervalProp = new JsonObject(); + intervalProp.addProperty("type", "integer"); + intervalProp.addProperty("description", "Interval between sends in milliseconds (default: 0)"); + intervalProp.addProperty("default", 0); + schema.add("interval_ms", intervalProp); + + JsonObject modificationsProp = new JsonObject(); + modificationsProp.addProperty("type", "array"); + modificationsProp.addProperty("description", "Array of modification rules to apply to the packet"); + JsonObject modificationItem = new JsonObject(); + modificationItem.addProperty("type", "object"); + JsonObject modificationProps = new JsonObject(); + + JsonObject targetProp = new JsonObject(); + targetProp.addProperty("type", "string"); + targetProp.addProperty("enum", "[\"request\", \"response\", \"both\"]"); + targetProp.addProperty("description", "Target to modify: request, response, or both"); + modificationProps.add("target", targetProp); + + JsonObject typeProp = new JsonObject(); + typeProp.addProperty("type", "string"); + typeProp.addProperty("enum", "[\"regex_replace\", \"header_add\", \"header_modify\"]"); + typeProp.addProperty("description", "Type of modification"); + modificationProps.add("type", typeProp); + + JsonObject patternProp = new JsonObject(); + patternProp.addProperty("type", "string"); + patternProp.addProperty("description", "Regex pattern for regex_replace type"); + modificationProps.add("pattern", patternProp); + + JsonObject replacementProp = new JsonObject(); + replacementProp.addProperty("type", "string"); + replacementProp.addProperty("description", "Replacement string for regex_replace or value for headers"); + modificationProps.add("replacement", replacementProp); + + JsonObject nameProp = new JsonObject(); + nameProp.addProperty("type", "string"); + nameProp.addProperty("description", "Header name for header_add/header_modify"); + modificationProps.add("name", nameProp); + + JsonObject valueProp = new JsonObject(); + valueProp.addProperty("type", "string"); + valueProp.addProperty("description", "Header value for header_add/header_modify"); + modificationProps.add("value", valueProp); + + modificationItem.add("properties", modificationProps); + modificationsProp.add("items", modificationItem); + schema.add("modifications", modificationsProp); + + JsonObject asyncProp = new JsonObject(); + asyncProp.addProperty("type", "boolean"); + asyncProp.addProperty("description", "Execute asynchronously (default: false)"); + asyncProp.addProperty("default", false); + schema.add("async", asyncProp); + + JsonObject allowDuplicateHeadersProp = new JsonObject(); + allowDuplicateHeadersProp.addProperty("type", "boolean"); + allowDuplicateHeadersProp.addProperty("description", + "Allow duplicate headers when adding/modifying headers (default: false - replace existing headers)"); + allowDuplicateHeadersProp.addProperty("default", false); + schema.add("allow_duplicate_headers", allowDuplicateHeadersProp); + + // access_tokenを追加 + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("ResendPacketTool: Starting packet resend operation"); + + // パラメータ取得 + if (!arguments.has("packet_id")) { + throw new IllegalArgumentException("packet_id parameter is required"); + } + + int packetId = arguments.get("packet_id").getAsInt(); + int count = arguments.has("count") ? arguments.get("count").getAsInt() : 1; + int intervalMs = arguments.has("interval_ms") ? arguments.get("interval_ms").getAsInt() : 0; + boolean async = arguments.has("async") ? arguments.get("async").getAsBoolean() : false; + boolean allowDuplicateHeaders = arguments.has("allow_duplicate_headers") + ? arguments.get("allow_duplicate_headers").getAsBoolean() + : false; + + JsonArray modifications = arguments.has("modifications") + ? arguments.getAsJsonArray("modifications") + : new JsonArray(); + + log("ResendPacketTool: packet_id=" + packetId + ", count=" + count + ", interval=" + intervalMs + "ms, async=" + + async + ", allowDuplicateHeaders=" + allowDuplicateHeaders); + + // パケットを取得 + Packet originalPacket = Packets.getInstance().query(packetId); + if (originalPacket == null) { + throw new IllegalArgumentException("Packet with ID " + packetId + " not found"); + } + + // 適切なデータを使ってOneShotPacketを作成 + // 改変データがあれば改変データを、なければ送信データを使用 + OneShotPacket originalOneShot; + if (originalPacket.getModifiedData().length > 0) { + originalOneShot = originalPacket.getOneShotFromModifiedData(); + } else if (originalPacket.getSentData().length > 0) { + originalOneShot = originalPacket.getOneShotPacket(originalPacket.getSentData()); + } else { + // デコードされたデータをフォールバックとして使用 + originalOneShot = originalPacket.getOneShotFromDecodedData(); + } + + if (originalOneShot == null) { + throw new IllegalArgumentException("Cannot create OneShotPacket from packet ID " + packetId); + } + + log("ResendPacketTool: Original packet found, preparing for resend"); + + long startTime = System.currentTimeMillis(); + int sentCount = 0; + int failedCount = 0; + List newPacketIds = new ArrayList<>(); + + try { + ResendController resendController = ResendController.getInstance(); + + if (count == 1 && modifications.size() == 0) { + // 単純な1回再送、改変なし + log("ResendPacketTool: Simple single resend without modifications"); + resendController.resend(originalOneShot); + sentCount = 1; + } else { + // 複数回送信または改変ありの場合 + log("ResendPacketTool: Complex resend with count=" + count + " and modifications=" + + modifications.size()); + + for (int i = 0; i < count; i++) { + try { + OneShotPacket modifiedPacket = applyModifications(originalOneShot, modifications, i + 1, + allowDuplicateHeaders); + resendController.resend(modifiedPacket); + sentCount++; + + // インターバル待機(最後の送信後は待機しない) + if (intervalMs > 0 && i < count - 1) { + Thread.sleep(intervalMs); + } + } catch (Exception e) { + log("ResendPacketTool: Failed to send packet " + (i + 1) + ": " + e.getMessage()); + failedCount++; + } + } + } + } catch (Exception e) { + log("ResendPacketTool: Resend operation failed: " + e.getMessage()); + failedCount = count - sentCount; + throw e; + } + + long executionTime = System.currentTimeMillis() - startTime; + + // 結果作成 + JsonObject result = new JsonObject(); + result.addProperty("success", failedCount == 0); + result.addProperty("sent_count", sentCount); + result.addProperty("failed_count", failedCount); + result.addProperty("execution_time_ms", executionTime); + + JsonArray packetIdsArray = new JsonArray(); + for (Integer id : newPacketIds) { + packetIdsArray.add(id); + } + result.add("packet_ids", packetIdsArray); + + log("ResendPacketTool: Completed. Sent: " + sentCount + ", Failed: " + failedCount + ", Time: " + executionTime + + "ms"); + return result; + } + + /** + * パケットに改変を適用 + */ + private OneShotPacket applyModifications(OneShotPacket original, JsonArray modifications, int index, + boolean allowDuplicateHeaders) throws Exception { + if (modifications.size() == 0) { + return original; + } + + log("ResendPacketTool: Applying " + modifications.size() + " modifications to packet (index=" + index + ")"); + + byte[] data = original.getData().clone(); + String dataStr = new String(data); + + for (JsonElement modElement : modifications) { + JsonObject modification = modElement.getAsJsonObject(); + + String target = modification.has("target") ? modification.get("target").getAsString() : "request"; + String type = modification.get("type").getAsString(); + + log("ResendPacketTool: Applying modification type=" + type + ", target=" + target); + + switch (type) { + case "regex_replace" : + dataStr = applyRegexReplace(dataStr, modification, index); + break; + case "header_add" : + dataStr = applyHeaderAdd(dataStr, modification, index, allowDuplicateHeaders); + break; + case "header_modify" : + dataStr = applyHeaderModify(dataStr, modification, index, allowDuplicateHeaders); + break; + default : + log("ResendPacketTool: Unknown modification type: " + type); + break; + } + } + + data = dataStr.getBytes(); + + // 新しいOneShotPacketを作成 + OneShotPacket modifiedPacket = new OneShotPacket(original.getId(), original.getListenPort(), + original.getClient(), original.getServer(), original.getServerName(), original.getUseSSL(), data, + original.getEncoder(), original.getAlpn(), original.getDirection(), original.getConn(), + original.getGroup()); + + return modifiedPacket; + } + + /** + * 正規表現置換を適用 + */ + private String applyRegexReplace(String data, JsonObject modification, int index) { + String pattern = modification.get("pattern").getAsString(); + String replacement = modification.get("replacement").getAsString(); + + // 置換変数を処理 + replacement = processReplacementVariables(replacement, index); + + try { + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(data); + String result = matcher.replaceAll(replacement); + log("ResendPacketTool: Regex replace applied - pattern: " + pattern + ", replacement: " + replacement); + return result; + } catch (Exception e) { + log("ResendPacketTool: Regex replace failed: " + e.getMessage()); + return data; + } + } + + /** + * ヘッダー追加を適用 + */ + private String applyHeaderAdd(String data, JsonObject modification, int index, boolean allowDuplicateHeaders) { + String name = modification.get("name").getAsString(); + String value = modification.get("value").getAsString(); + + // 置換変数を処理 + value = processReplacementVariables(value, index); + + // HTTP形式のデータの場合、ヘッダー部分に追加 + if (data.contains("\r\n\r\n")) { + int headerEnd = data.indexOf("\r\n\r\n"); + String headers = data.substring(0, headerEnd); + String body = data.substring(headerEnd); + + // 重複を許可しない場合、既存ヘッダーがあるかチェック + if (!allowDuplicateHeaders) { + String pattern = "(?i)" + Pattern.quote(name) + ":\\s*[^\r\n]*"; + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(headers); + if (matcher.find()) { + // 既存ヘッダーを置換 + String result = matcher.replaceFirst(name + ": " + value) + body; + log("ResendPacketTool: Header replaced (no duplicates allowed) - " + name + ": " + value); + return result; + } + } + + // 新しいヘッダーを追加 + String newHeader = name + ": " + value + "\r\n"; + String result = headers + "\r\n" + newHeader + body; + log("ResendPacketTool: Header added - " + name + ": " + value); + return result; + } + + return data; + } + + /** + * ヘッダー変更を適用 + */ + private String applyHeaderModify(String data, JsonObject modification, int index, boolean allowDuplicateHeaders) { + String name = modification.get("name").getAsString(); + String value = modification.get("value").getAsString(); + + // 置換変数を処理 + value = processReplacementVariables(value, index); + + // 既存ヘッダーを置換 + String pattern = "(?i)" + Pattern.quote(name) + ":\\s*[^\r\n]*"; + + try { + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(data); + if (matcher.find()) { + String replacement = name + ": " + value; + String result; + if (allowDuplicateHeaders) { + // 重複を許可する場合は最初のヘッダーのみ変更 + result = matcher.replaceFirst(replacement); + } else { + // 重複を許可しない場合は全ての同名ヘッダーを置換 + result = matcher.replaceAll(replacement); + } + log("ResendPacketTool: Header modified - " + name + ": " + value + " (allowDuplicates=" + + allowDuplicateHeaders + ")"); + return result; + } else { + // ヘッダーが見つからない場合は追加 + return applyHeaderAdd(data, modification, index, allowDuplicateHeaders); + } + } catch (Exception e) { + log("ResendPacketTool: Header modify failed: " + e.getMessage()); + return data; + } + } + + /** + * 置換変数を処理 + */ + private String processReplacementVariables(String input, int index) { + String result = input; + + // {{index}} - 送信順序 + result = result.replace("{{index}}", String.valueOf(index)); + + // {{timestamp}} - Unix timestamp + result = result.replace("{{timestamp}}", String.valueOf(System.currentTimeMillis() / 1000)); + + // {{random}} - ランダム文字列 + if (result.contains("{{random}}")) { + String randomStr = generateRandomString(8); + result = result.replace("{{random}}", randomStr); + } + + // {{uuid}} - UUID v4 + if (result.contains("{{uuid}}")) { + String uuid = UUID.randomUUID().toString(); + result = result.replace("{{uuid}}", uuid); + } + + // {{datetime}} - ISO 8601形式日時 + if (result.contains("{{datetime}}")) { + String datetime = Instant.now().atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT); + result = result.replace("{{datetime}}", datetime); + } + + return result; + } + + /** + * ランダム文字列生成 + */ + private String generateRandomString(int length) { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < length; i++) { + sb.append(chars.charAt(random.nextInt(chars.length()))); + } + + return sb.toString(); + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java index 870b82d7..c9fc9ecd 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java @@ -109,7 +109,7 @@ private JsonObject loadBackupConfig(String backupId) throws Exception { try (FileReader reader = new FileReader(backupFile)) { JsonObject backupConfig = gson.fromJson(reader, JsonObject.class); - + if (backupConfig == null) { throw new Exception("Invalid backup file format"); } @@ -125,4 +125,4 @@ private JsonObject loadBackupConfig(String backupId) throws Exception { throw new Exception("Failed to parse backup file: " + e.getMessage()); } } -} \ No newline at end of file +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java index dd62464d..ca8e6723 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java @@ -22,6 +22,7 @@ private void registerDefaultTools() { registerTool(new UpdateConfigTool()); registerTool(new RestoreConfigTool()); registerTool(new LogTool()); + registerTool(new ResendPacketTool()); } public void registerTool(MCPTool tool) { diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java index d0a7450d..10b16b70 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java @@ -4,28 +4,17 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import java.io.BufferedReader; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; -import packetproxy.model.ListenPort; -import packetproxy.model.ListenPorts; -import packetproxy.model.Modification; -import packetproxy.model.Modifications; -import packetproxy.model.SSLPassThrough; -import packetproxy.model.SSLPassThroughs; -import packetproxy.common.ConfigIO; -import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; -import packetproxy.model.Server; -import packetproxy.model.Servers; +import java.text.SimpleDateFormat; +import java.util.Date; public class UpdateConfigTool extends AuthenticatedMCPTool { @@ -73,7 +62,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception try { log("UpdateConfigTool step 1: Starting configuration update"); - + JsonObject backupInfo = null; if (backup) { @@ -124,15 +113,15 @@ private JsonObject createConfigBackup() throws Exception { Date now = new Date(); String timestamp = dateFormat.format(now); String backupId = "backup_" + timestamp.replace(":", "").replace("-", "").replace("T", "_").replace("Z", ""); - + // Create backup directory if it doesn't exist File backupDir = new File("backup"); if (!backupDir.exists()) { backupDir.mkdirs(); } - + String backupPath = backupDir.getPath() + File.separator + backupId + ".json"; - + try { // HTTP APIで設定を直接取得(認証チェックを回避) String configText = getConfigFromHttpApiForBackup(); @@ -154,7 +143,7 @@ private JsonObject createConfigBackup() throws Exception { log("Failed to create backup: " + e.getMessage()); throw new Exception("Failed to create configuration backup: " + e.getMessage()); } - + JsonObject backupInfo = new JsonObject(); backupInfo.addProperty("backup_id", backupId); backupInfo.addProperty("backup_path", backupPath); @@ -167,17 +156,17 @@ private JsonObject createConfigBackup() throws Exception { private void updateConfiguration(JsonObject configJson) throws Exception { log("UpdateConfigTool starting configuration update using HTTP API"); - + // HTTP POST APIで設定を更新(削除処理も自動実行) updateConfigViaHttpApi(configJson.toString()); - + log("UpdateConfigTool configuration update completed using HTTP API"); } - + private void updateConfigViaHttpApi(String configJsonString) throws Exception { // 設定済みAccessTokenを取得(HTTPリクエスト用) String accessToken = getConfiguredAccessToken(); - + // HTTP POSTリクエスト URL url = new URL("http://localhost:32349/config"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); @@ -187,13 +176,13 @@ private void updateConfigViaHttpApi(String configJsonString) throws Exception { conn.setDoOutput(true); conn.setConnectTimeout(5000); conn.setReadTimeout(10000); - + // リクエストボディを送信 try (OutputStream os = conn.getOutputStream()) { byte[] input = configJsonString.getBytes("utf-8"); os.write(input, 0, input.length); } - + int responseCode = conn.getResponseCode(); if (responseCode != 200) { // エラーレスポンスがある場合は読み取り @@ -210,9 +199,10 @@ private void updateConfigViaHttpApi(String configJsonString) throws Exception { } } } - throw new Exception(errorMessage + ". Check if config sharing is enabled and user confirmed the operation."); + throw new Exception( + errorMessage + ". Check if config sharing is enabled and user confirmed the operation."); } - + // 成功レスポンスを読み取り(必要に応じて) try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { StringBuilder response = new StringBuilder(); @@ -222,14 +212,14 @@ private void updateConfigViaHttpApi(String configJsonString) throws Exception { } log("HTTP API response: " + response.toString()); } - + conn.disconnect(); } - + private String getConfigFromHttpApiForBackup() throws Exception { // 設定済みAccessTokenを取得(HTTPリクエスト用) String accessToken = getConfiguredAccessToken(); - + // HTTP GETリクエスト URL url = new URL("http://localhost:32349/config"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); @@ -237,12 +227,12 @@ private String getConfigFromHttpApiForBackup() throws Exception { conn.setRequestProperty("Authorization", accessToken); conn.setConnectTimeout(5000); conn.setReadTimeout(10000); - + int responseCode = conn.getResponseCode(); if (responseCode != 200) { throw new Exception("HTTP API returned status: " + responseCode + ". Check if config sharing is enabled."); } - + // レスポンスを読み取り BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); StringBuilder response = new StringBuilder(); @@ -252,8 +242,8 @@ private String getConfigFromHttpApiForBackup() throws Exception { } reader.close(); conn.disconnect(); - + return response.toString(); } -} \ No newline at end of file +} From e8d0b67d6b90db79a1a6761f682b68a0f7f3367a Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 4 Aug 2025 18:14:03 +0900 Subject: [PATCH 10/39] =?UTF-8?q?=E4=BB=95=E6=A7=98=E6=9B=B8=E3=82=92?= =?UTF-8?q?=E7=8F=BE=E5=9C=A8=E3=81=AE=E5=AE=9F=E8=A3=85=E3=81=AB=E5=90=88?= =?UTF-8?q?=E3=82=8F=E3=81=9B=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-setting-guide.md | 8 -- doc/mcp-server-spec.md | 191 +------------------------------- 2 files changed, 3 insertions(+), 196 deletions(-) diff --git a/doc/mcp-server-setting-guide.md b/doc/mcp-server-setting-guide.md index 1d11fec5..142a328b 100644 --- a/doc/mcp-server-setting-guide.md +++ b/doc/mcp-server-setting-guide.md @@ -229,14 +229,6 @@ PacketProxyの現在の設定を確認してください パケットID 123の詳細情報を取得してください ``` -## 今後の拡張予定 - -- 完全なGUI連携機能 -- パケット再送機能 (`resend_packet`) -- 設定変更機能 (`update_config`) -- フィルタ機能 (`get_filters`, `validate_filter`) -- HTTP REST API対応 - ## サポート 問題が発生した場合は、以下を確認してください: diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index 7c8e093d..d6f5e214 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -438,189 +438,7 @@ PacketProxyのログを取得します。 } ``` -### 7. `get_filters` - フィルタ定義取得 - -保存済みフィルタと利用可能なカラム情報を取得します。 - -**リクエスト:** - -```json -{ - "jsonrpc": "2.0", - "method": "tools/call", - "params": { - "name": "get_filters", - "arguments": { - "include_examples": true - } - }, - "id": 7 -} -``` - -**パラメータ:** -- `include_examples` (boolean, optional): サンプルフィルタも含める (デフォルト: false) - -**レスポンス:** - -```json -{ - "jsonrpc": "2.0", - "result": { - "saved_filters": [ - { - "id": 1, - "name": "API Requests", - "filter": "url =~ /api/ && method == GET" - } - ], - "available_columns": [ - { - "name": "id", - "type": "integer", - "description": "パケットID" - }, - { - "name": "request", - "type": "string", - "description": "リクエスト内容" - } - ], - "operators": [ - { - "operator": "==", - "description": "等しい", - "example": "method == GET" - } - ], - "example_filters": [ - { - "name": "HTTP Errors", - "filter": "status >= 400 && status <= 599" - } - ] - }, - "id": 7 -} -``` - -### 8. `validate_filter` - フィルタ構文検証 - -フィルタ構文の妥当性を検証します。 - -**リクエスト:** - -```json -{ - "jsonrpc": "2.0", - "method": "tools/call", - "params": { - "name": "validate_filter", - "arguments": { - "filter": "method == GET && url =~ /api/", - "explain": true - } - }, - "id": 8 -} -``` - -**パラメータ:** -- `filter` (string, required): 検証するフィルタ構文 -- `explain` (boolean, optional): 詳細説明を含める (デフォルト: false) - -**成功レスポンス:** - -```json -{ - "jsonrpc": "2.0", - "result": { - "valid": true, - "explanation": { - "description": "HTTPメソッドがGETかつURLに'/api/'が含まれるパケットをフィルタ", - "parsed_conditions": [ - { - "column": "method", - "operator": "==", - "value": "GET", - "description": "HTTPメソッドがGETと等しい" - } - ], - "logical_operator": "AND" - }, - "estimated_performance": "fast", - "recommendations": [ - "正規表現パフォーマンスが良好です" - ] - }, - "id": 8 -} -``` - -**エラーレスポンス:** - -```json -{ - "jsonrpc": "2.0", - "result": { - "valid": false, - "errors": [ - { - "type": "syntax_error", - "message": "Invalid operator '===' at position 15", - "position": 15, - "context": "method === GET" - } - ], - "suggestions": [ - "Use single '=' instead of '==='" - ] - }, - "id": 8 -} -``` - -### 9. `get_config_backups` - 設定バックアップ一覧取得 - -作成された設定バックアップの一覧を取得します。 - -**リクエスト:** - -```json -{ - "jsonrpc": "2.0", - "method": "tools/call", - "params": { - "name": "get_config_backups", - "arguments": { - "limit": 10 - } - }, - "id": 9 -} -``` - -**レスポンス:** - -```json -{ - "jsonrpc": "2.0", - "result": { - "backups": [ - { - "backup_id": "backup_20250115_103000", - "timestamp": "2025-01-15T10:30:00Z", - "size_bytes": 2048, - "description": "Backup before MCP config update" - } - ], - "total_count": 5 - }, - "id": 9 -} -``` - -### 10. `restore_config` - 設定バックアップ復元 +### 7. `restore_config` - 設定バックアップ復元 指定したバックアップから設定を復元します。 @@ -637,7 +455,7 @@ PacketProxyのログを取得します。 "backup_id": "backup_20250115_103000" } }, - "id": 10 + "id": 7 } ``` @@ -658,7 +476,7 @@ PacketProxyのログを取得します。 } ] }, - "id": 10 + "id": 7 } ``` @@ -743,9 +561,6 @@ GET /mcp/configs # 設定一覧 PUT /mcp/configs # 設定更新 POST /mcp/resend/{packet_id} # パケット再送 GET /mcp/logs?level=info # ログ取得 -GET /mcp/filters # フィルタ一覧 -POST /mcp/filters/validate # フィルタ検証 -GET /mcp/backups # バックアップ一覧 POST /mcp/restore/{backup_id} # バックアップ復元 ``` From 593b648f32ebc1dc9a9038fc2d62b31bd4b5efc0 Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 4 Aug 2025 18:37:11 +0900 Subject: [PATCH 11/39] =?UTF-8?q?=E6=97=A5=E4=BB=98=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index d6f5e214..4b1e5871 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -645,5 +645,5 @@ src/main/java/core/packetproxy/extensions/mcp/ | バージョン | 日付 | 変更内容 | |-------|------------|------| -| 1.0.0 | 2025-01-15 | 初版作成 | +| 1.0.0 | 2025-08-04 | 初版作成 | From 6169ae70b477c69847eb4c8886877d3017629b29 Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 4 Aug 2025 21:10:29 +0900 Subject: [PATCH 12/39] =?UTF-8?q?cline=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/mcp-http-bridge.js | 129 +++++++++++++++--- .../packetproxy/extensions/mcp/MCPServer.java | 9 ++ .../mcp/tools/ResendPacketTool.java | 12 +- 3 files changed, 130 insertions(+), 20 deletions(-) diff --git a/scripts/mcp-http-bridge.js b/scripts/mcp-http-bridge.js index 101733e3..e027beec 100755 --- a/scripts/mcp-http-bridge.js +++ b/scripts/mcp-http-bridge.js @@ -7,9 +7,66 @@ const http = require('http'); const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); // Configuration const PACKETPROXY_HTTP_URL = 'http://localhost:8765/mcp'; +const LOCK_FILE = path.join(os.tmpdir(), 'mcp-http-bridge.lock'); + +// Single instance enforcement +function ensureSingleInstance() { + try { + // Check if lock file exists and process is still running + if (fs.existsSync(LOCK_FILE)) { + const pidStr = fs.readFileSync(LOCK_FILE, 'utf8').trim(); + const pid = parseInt(pidStr, 10); + + if (!isNaN(pid)) { + try { + // Check if process is still running + process.kill(pid, 0); + console.error(`[ERROR] Another instance is already running (PID: ${pid})`); + process.exit(1); + } catch (err) { + // Process not running, remove stale lock file + fs.unlinkSync(LOCK_FILE); + } + } else { + // Invalid PID in lock file, remove it + fs.unlinkSync(LOCK_FILE); + } + } + + // Create lock file with current PID + fs.writeFileSync(LOCK_FILE, process.pid.toString()); + + // Clean up lock file on exit + const cleanup = () => { + try { + if (fs.existsSync(LOCK_FILE)) { + fs.unlinkSync(LOCK_FILE); + } + } catch (err) { + // Ignore cleanup errors + } + }; + + process.on('exit', cleanup); + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); + process.on('uncaughtException', (err) => { + console.error('[FATAL] Uncaught exception:', err); + cleanup(); + process.exit(1); + }); + + } catch (error) { + console.error(`[ERROR] Failed to ensure single instance: ${error.message}`); + process.exit(1); + } +} // MCP Server implementation class MCPHttpBridge { @@ -22,7 +79,7 @@ class MCPHttpBridge { } async handleRequest(request) { - console.error(`[DEBUG] Processing request: ${request.method}`); + console.log(`[DEBUG] Processing request: ${request.method}`); switch (request.method) { case 'initialize': @@ -35,6 +92,8 @@ class MCPHttpBridge { return this.handleToolsCall(request); case 'resources/list': return this.handleResourcesList(request); + case 'resources/templates/list': + return this.handleResourcesTemplatesList(request); case 'prompts/list': return this.handlePromptsList(request); default: @@ -44,7 +103,7 @@ class MCPHttpBridge { async handleInitialize(request) { this.initialized = true; - console.error('[DEBUG] Initialize request - forwarding to PacketProxy'); + console.log('[DEBUG] Initialize request - forwarding to PacketProxy'); try { const response = await this.forwardToPacketProxy(request); @@ -56,13 +115,24 @@ class MCPHttpBridge { } async handleNotificationInitialized(request) { - console.error('[DEBUG] Notification initialized received (no response needed)'); + console.log('[DEBUG] Notification initialized received (no response needed)'); // Notifications don't require a response, return null return null; } async handleResourcesList(request) { - console.error('[DEBUG] Resources list request - forwarding to PacketProxy'); + console.log('[DEBUG] Resources list request - forwarding to PacketProxy'); + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } + } + + async handleResourcesTemplatesList(request) { + console.log('[DEBUG] Resources templates list request - forwarding to PacketProxy'); try { const response = await this.forwardToPacketProxy(request); return response; @@ -73,7 +143,7 @@ class MCPHttpBridge { } async handlePromptsList(request) { - console.error('[DEBUG] Prompts list request - forwarding to PacketProxy'); + console.log('[DEBUG] Prompts list request - forwarding to PacketProxy'); try { const response = await this.forwardToPacketProxy(request); return response; @@ -88,7 +158,7 @@ class MCPHttpBridge { return this.createErrorResponse(request.id, -32002, "Server not initialized"); } - console.error('[DEBUG] Tools list request - forwarding to PacketProxy'); + console.log('[DEBUG] Tools list request - forwarding to PacketProxy'); try { const response = await this.forwardToPacketProxy(request); @@ -104,7 +174,7 @@ class MCPHttpBridge { return this.createErrorResponse(request.id, -32002, "Server not initialized"); } - console.error(`[DEBUG] Tools call request: ${request.params?.name}`); + console.log(`[DEBUG] Tools call request: ${request.params?.name}`); try { const response = await this.forwardToPacketProxy(request); @@ -119,14 +189,16 @@ class MCPHttpBridge { return new Promise((resolve, reject) => { const postData = JSON.stringify(request); - console.error(`[DEBUG] Forwarding to PacketProxy: ${postData}`); + console.log(`[DEBUG] Forwarding to PacketProxy: ${postData}`); + console.log(`[DEBUG] Target URL: ${PACKETPROXY_HTTP_URL}`); const options = { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) - } + }, + timeout: 5000 // 5 second timeout }; const req = http.request(PACKETPROXY_HTTP_URL, options, (res) => { @@ -137,20 +209,38 @@ class MCPHttpBridge { }); res.on('end', () => { + console.log(`[DEBUG] Raw response from PacketProxy (status: ${res.statusCode}): ${data}`); + + if (res.statusCode !== 200) { + console.error(`[ERROR] HTTP error from PacketProxy: ${res.statusCode} ${res.statusMessage}`); + reject(new Error(`HTTP error: ${res.statusCode} ${res.statusMessage}`)); + return; + } + try { const response = JSON.parse(data); - console.error(`[DEBUG] PacketProxy response: ${data}`); + console.log(`[DEBUG] PacketProxy response parsed successfully`); resolve(response); } catch (error) { + console.error(`[ERROR] Failed to parse PacketProxy response: ${error.message}`); + console.error(`[ERROR] Raw data: ${data}`); reject(new Error(`Failed to parse PacketProxy response: ${error.message}`)); } }); }); req.on('error', (error) => { + console.error(`[ERROR] HTTP request to PacketProxy failed: ${error.message}`); + console.error(`[ERROR] Error code: ${error.code}`); reject(new Error(`HTTP request failed: ${error.message}`)); }); + req.on('timeout', () => { + console.error(`[ERROR] HTTP request to PacketProxy timed out`); + req.destroy(); + reject(new Error('HTTP request timed out')); + }); + req.write(postData); req.end(); }); @@ -170,7 +260,10 @@ class MCPHttpBridge { // Main execution async function main() { - console.error('[DEBUG] PacketProxy MCP HTTP Bridge starting...'); + // Ensure only one instance can run + ensureSingleInstance(); + + console.log('[DEBUG] PacketProxy MCP HTTP Bridge starting...'); const bridge = new MCPHttpBridge(); @@ -190,17 +283,17 @@ async function main() { if (!trimmedLine) continue; try { - console.error(`[DEBUG] Received: ${trimmedLine}`); + console.log(`[DEBUG] Received: ${trimmedLine}`); const request = JSON.parse(trimmedLine); const response = await bridge.handleRequest(request); // Only send response if it's not null (notifications don't need responses) if (response !== null) { const responseStr = JSON.stringify(response); - console.log(responseStr); - console.error(`[DEBUG] Sent: ${responseStr}`); + process.stdout.write(responseStr + '\n'); + console.log(`[DEBUG] Sent: ${responseStr.length} characters`); } else { - console.error(`[DEBUG] No response needed (notification)`); + console.log(`[DEBUG] No response needed (notification)`); } } catch (error) { console.error(`[ERROR] Failed to process request: ${error.message}`); @@ -221,19 +314,19 @@ async function main() { message: "Parse error" } }; - console.log(JSON.stringify(errorResponse)); + process.stdout.write(JSON.stringify(errorResponse) + '\n'); } } }); process.stdin.on('end', () => { - console.error('[DEBUG] Bridge shutting down'); + console.log('[DEBUG] Bridge shutting down'); process.exit(0); }); // Handle process termination process.on('SIGINT', () => { - console.error('[DEBUG] Received SIGINT, shutting down'); + console.log('[DEBUG] Received SIGINT, shutting down'); process.exit(0); }); } diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java index 04463503..704c36ce 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java @@ -172,6 +172,8 @@ private JsonObject handleMethod(String method, JsonObject params) throws Excepti return handleToolsCall(params); case "resources/list" : return handleResourcesList(); + case "resources/templates/list" : + return handleResourcesTemplatesList(); case "prompts/list" : return handlePromptsList(); default : @@ -223,6 +225,13 @@ private JsonObject handleResourcesList() { return result; } + private JsonObject handleResourcesTemplatesList() { + JsonObject result = new JsonObject(); + JsonObject[] resourceTemplates = {}; + result.add("resourceTemplates", gson.toJsonTree(resourceTemplates)); + return result; + } + private JsonObject handlePromptsList() { JsonObject result = new JsonObject(); JsonObject[] prompts = {}; diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java index e219261d..4d2bfc56 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java @@ -64,13 +64,21 @@ public JsonObject getInputSchema() { JsonObject targetProp = new JsonObject(); targetProp.addProperty("type", "string"); - targetProp.addProperty("enum", "[\"request\", \"response\", \"both\"]"); + JsonArray targetEnum = new JsonArray(); + targetEnum.add("request"); + targetEnum.add("response"); + targetEnum.add("both"); + targetProp.add("enum", targetEnum); targetProp.addProperty("description", "Target to modify: request, response, or both"); modificationProps.add("target", targetProp); JsonObject typeProp = new JsonObject(); typeProp.addProperty("type", "string"); - typeProp.addProperty("enum", "[\"regex_replace\", \"header_add\", \"header_modify\"]"); + JsonArray typeEnum = new JsonArray(); + typeEnum.add("regex_replace"); + typeEnum.add("header_add"); + typeEnum.add("header_modify"); + typeProp.add("enum", typeEnum); typeProp.addProperty("description", "Type of modification"); modificationProps.add("type", typeProp); From 46ae657b94cef0fa2f91afa915bf8a5c4bf6a1a1 Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 4 Aug 2025 21:25:49 +0900 Subject: [PATCH 13/39] =?UTF-8?q?PacketProxy=E3=81=AEAccessToken=E3=82=92?= =?UTF-8?q?=20mcpServers=20=E3=81=AE=20env=20=E3=81=8B=E3=82=89=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=80=82env=E5=91=A8=E3=82=8A=E3=81=AE?= =?UTF-8?q?=E8=AA=AC=E6=98=8E=E3=82=82=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-setting-guide.md | 22 +++++++++-- doc/mcp-server-spec.md | 48 ++++++++++++++++++++++ scripts/mcp-http-bridge.js | 70 ++++++++++++++++++++++++--------- 3 files changed, 118 insertions(+), 22 deletions(-) diff --git a/doc/mcp-server-setting-guide.md b/doc/mcp-server-setting-guide.md index 142a328b..7ea0148f 100644 --- a/doc/mcp-server-setting-guide.md +++ b/doc/mcp-server-setting-guide.md @@ -37,7 +37,15 @@ MCP Server started HTTP endpoint available at http://localhost:8765/mcp ``` -### 2. Claude Desktop設定ファイルの編集 +### 2. アクセストークンの取得 + +PacketProxy GUIでアクセストークンを有効化し、トークンを取得します: + +1. **Options** → **Setting** を選択 +2. **Import/Export configs** セクションで **Enable** にチェックを入れる +3. 表示されたアクセストークンをコピーしておく + +### 3. Claude Desktop設定ファイルの編集 Claude Desktopの設定ファイルを編集します: @@ -46,7 +54,7 @@ Claude Desktopの設定ファイルを編集します: open ~/Library/Application\ Support/Claude/claude_desktop_config.json ``` -以下の内容を追加してください: +以下の内容を追加してください(`your_access_token_here`の部分を手順2で取得したアクセストークンに置き換えてください): ```json { @@ -55,7 +63,10 @@ open ~/Library/Application\ Support/Claude/claude_desktop_config.json "command": "node", "args": [ "/Users/kakira/PacketProxy/scripts/mcp-http-bridge.js" - ] + ], + "env": { + "PACKET_PROXY_ACCESS_TOKEN": "your_access_token_here" + } } } } @@ -74,7 +85,10 @@ open ~/Library/Application\ Support/Claude/claude_desktop_config.json "command": "node", "args": [ "/Users/kakira/PacketProxy/scripts/mcp-http-bridge.js" - ] + ], + "env": { + "PACKET_PROXY_ACCESS_TOKEN": "your_access_token_here" + } } } } diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index 4b1e5871..02a19588 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -38,11 +38,22 @@ PacketProxy MCP サーバーは、Model Context Protocol (MCP) を使用してPa 4. 自動生成された**AccessToken**をコピーする 5. MCPツール呼び出し時に`access_token`パラメータとして使用する +### 環境変数による自動認証 (推奨) + +MCP HTTP Bridgeを使用する場合、環境変数にアクセストークンを設定することで、自動的に認証情報が追加されます: + +```bash +export PACKET_PROXY_ACCESS_TOKEN="your_access_token_here" +``` + +この設定により、各ツール呼び出し時に手動で`access_token`パラメータを指定する必要がなくなります。 + ### 認証エラーの場合 - アクセストークンが未設定: PacketProxyでconfig sharingを有効にしてください - アクセストークンが無効: Settings画面で正しいトークンを確認してください - アクセストークンが空: 必須パラメータのため、必ず指定してください +- 環境変数が設定されていない場合: `PACKET_PROXY_ACCESS_TOKEN`環境変数を確認してください ## MCPツール一覧 @@ -615,6 +626,43 @@ Authorization: Bearer 3. **キャッシュ**: 頻繁にアクセスされるデータはキャッシュ 4. **非同期処理**: 時間のかかる操作は非同期実行をサポート +## 環境変数 + +### MCP HTTP Bridge 環境変数 + +MCP HTTP Bridgeは以下の環境変数をサポートします: + +#### `PACKET_PROXY_ACCESS_TOKEN` +- **説明**: PacketProxyのアクセストークン +- **必須**: 推奨 (手動指定の代替) +- **形式**: 文字列 +- **例**: `export PACKET_PROXY_ACCESS_TOKEN="abc123def456"` +- **動作**: 設定時、すべてのMCPツール呼び出しに自動的にアクセストークンが追加されます + +#### `MCP_DEBUG` +- **説明**: デバッグログ出力制御 +- **必須**: オプション +- **形式**: `"true"` または `"false"` +- **デフォルト**: `"false"` +- **例**: `export MCP_DEBUG="true"` +- **動作**: + - `"true"`: デバッグメッセージをstderrに出力 + - `"false"`: デバッグメッセージを出力しない (JSON-RPC通信を汚染しない) + +#### 使用例 + +```bash +# 基本設定 +export PACKET_PROXY_ACCESS_TOKEN="your_access_token_here" + +# デバッグ有効化 +export MCP_DEBUG="true" +export PACKET_PROXY_ACCESS_TOKEN="your_access_token_here" + +# MCP HTTP Bridge起動 +node /path/to/mcp-http-bridge.js +``` + ## 実装詳細 ### ディレクトリ構成 diff --git a/scripts/mcp-http-bridge.js b/scripts/mcp-http-bridge.js index e027beec..28a9f6ee 100755 --- a/scripts/mcp-http-bridge.js +++ b/scripts/mcp-http-bridge.js @@ -14,6 +14,14 @@ const os = require('os'); // Configuration const PACKETPROXY_HTTP_URL = 'http://localhost:8765/mcp'; const LOCK_FILE = path.join(os.tmpdir(), 'mcp-http-bridge.lock'); +const DEBUG_MODE = process.env.MCP_DEBUG === 'true'; + +// Debug logging function +function debugLog(message) { + if (DEBUG_MODE) { + console.error(message); + } +} // Single instance enforcement function ensureSingleInstance() { @@ -79,7 +87,7 @@ class MCPHttpBridge { } async handleRequest(request) { - console.log(`[DEBUG] Processing request: ${request.method}`); + debugLog(`[DEBUG] Processing request: ${request.method}`); switch (request.method) { case 'initialize': @@ -103,7 +111,7 @@ class MCPHttpBridge { async handleInitialize(request) { this.initialized = true; - console.log('[DEBUG] Initialize request - forwarding to PacketProxy'); + debugLog('[DEBUG] Initialize request - forwarding to PacketProxy'); try { const response = await this.forwardToPacketProxy(request); @@ -115,13 +123,13 @@ class MCPHttpBridge { } async handleNotificationInitialized(request) { - console.log('[DEBUG] Notification initialized received (no response needed)'); + debugLog('[DEBUG] Notification initialized received (no response needed)'); // Notifications don't require a response, return null return null; } async handleResourcesList(request) { - console.log('[DEBUG] Resources list request - forwarding to PacketProxy'); + debugLog('[DEBUG] Resources list request - forwarding to PacketProxy'); try { const response = await this.forwardToPacketProxy(request); return response; @@ -132,7 +140,7 @@ class MCPHttpBridge { } async handleResourcesTemplatesList(request) { - console.log('[DEBUG] Resources templates list request - forwarding to PacketProxy'); + debugLog('[DEBUG] Resources templates list request - forwarding to PacketProxy'); try { const response = await this.forwardToPacketProxy(request); return response; @@ -143,7 +151,7 @@ class MCPHttpBridge { } async handlePromptsList(request) { - console.log('[DEBUG] Prompts list request - forwarding to PacketProxy'); + debugLog('[DEBUG] Prompts list request - forwarding to PacketProxy'); try { const response = await this.forwardToPacketProxy(request); return response; @@ -158,7 +166,7 @@ class MCPHttpBridge { return this.createErrorResponse(request.id, -32002, "Server not initialized"); } - console.log('[DEBUG] Tools list request - forwarding to PacketProxy'); + debugLog('[DEBUG] Tools list request - forwarding to PacketProxy'); try { const response = await this.forwardToPacketProxy(request); @@ -174,7 +182,7 @@ class MCPHttpBridge { return this.createErrorResponse(request.id, -32002, "Server not initialized"); } - console.log(`[DEBUG] Tools call request: ${request.params?.name}`); + debugLog(`[DEBUG] Tools call request: ${request.params?.name}`); try { const response = await this.forwardToPacketProxy(request); @@ -187,10 +195,36 @@ class MCPHttpBridge { async forwardToPacketProxy(request) { return new Promise((resolve, reject) => { + // 環境変数からアクセストークンを取得 + const accessToken = process.env.PACKET_PROXY_ACCESS_TOKEN; + debugLog(`[DEBUG] Environment variable PACKET_PROXY_ACCESS_TOKEN: ${accessToken ? '[SET]' : '[NOT SET]'}`); + debugLog(`[DEBUG] Request method: ${request.method}`); + debugLog(`[DEBUG] Request params: ${JSON.stringify(request.params)}`); + + // tools/callリクエストの場合、arguments内にアクセストークンを追加 + if (accessToken && request.method === 'tools/call' && request.params) { + if (!request.params.arguments) { + request.params.arguments = {}; + } + if (!request.params.arguments.access_token) { + request.params.arguments.access_token = accessToken; + debugLog(`[DEBUG] Added access token to tools/call arguments`); + } + } + // その他のリクエストでparamsが存在する場合 + else if (accessToken && request.params && typeof request.params === 'object') { + if (!request.params.access_token) { + request.params.access_token = accessToken; + debugLog(`[DEBUG] Added access token to request params`); + } + } else if (!accessToken) { + debugLog(`[WARNING] PACKET_PROXY_ACCESS_TOKEN environment variable not set`); + } + const postData = JSON.stringify(request); - console.log(`[DEBUG] Forwarding to PacketProxy: ${postData}`); - console.log(`[DEBUG] Target URL: ${PACKETPROXY_HTTP_URL}`); + debugLog(`[DEBUG] Forwarding to PacketProxy: ${postData}`); + debugLog(`[DEBUG] Target URL: ${PACKETPROXY_HTTP_URL}`); const options = { method: 'POST', @@ -209,7 +243,7 @@ class MCPHttpBridge { }); res.on('end', () => { - console.log(`[DEBUG] Raw response from PacketProxy (status: ${res.statusCode}): ${data}`); + debugLog(`[DEBUG] Raw response from PacketProxy (status: ${res.statusCode}): ${data}`); if (res.statusCode !== 200) { console.error(`[ERROR] HTTP error from PacketProxy: ${res.statusCode} ${res.statusMessage}`); @@ -219,7 +253,7 @@ class MCPHttpBridge { try { const response = JSON.parse(data); - console.log(`[DEBUG] PacketProxy response parsed successfully`); + debugLog(`[DEBUG] PacketProxy response parsed successfully`); resolve(response); } catch (error) { console.error(`[ERROR] Failed to parse PacketProxy response: ${error.message}`); @@ -263,7 +297,7 @@ async function main() { // Ensure only one instance can run ensureSingleInstance(); - console.log('[DEBUG] PacketProxy MCP HTTP Bridge starting...'); + debugLog('[DEBUG] PacketProxy MCP HTTP Bridge starting...'); const bridge = new MCPHttpBridge(); @@ -283,7 +317,7 @@ async function main() { if (!trimmedLine) continue; try { - console.log(`[DEBUG] Received: ${trimmedLine}`); + debugLog(`[DEBUG] Received: ${trimmedLine}`); const request = JSON.parse(trimmedLine); const response = await bridge.handleRequest(request); @@ -291,9 +325,9 @@ async function main() { if (response !== null) { const responseStr = JSON.stringify(response); process.stdout.write(responseStr + '\n'); - console.log(`[DEBUG] Sent: ${responseStr.length} characters`); + debugLog(`[DEBUG] Sent: ${responseStr.length} characters`); } else { - console.log(`[DEBUG] No response needed (notification)`); + debugLog(`[DEBUG] No response needed (notification)`); } } catch (error) { console.error(`[ERROR] Failed to process request: ${error.message}`); @@ -320,13 +354,13 @@ async function main() { }); process.stdin.on('end', () => { - console.log('[DEBUG] Bridge shutting down'); + debugLog('[DEBUG] Bridge shutting down'); process.exit(0); }); // Handle process termination process.on('SIGINT', () => { - console.log('[DEBUG] Received SIGINT, shutting down'); + debugLog('[DEBUG] Received SIGINT, shutting down'); process.exit(0); }); } From 36bbd416ac45a30098d88d7229bbe331e30756fc Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 4 Aug 2025 21:42:02 +0900 Subject: [PATCH 14/39] spotlessApply --- doc/mcp-server-spec.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index 02a19588..ebf1dac5 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -633,6 +633,7 @@ Authorization: Bearer MCP HTTP Bridgeは以下の環境変数をサポートします: #### `PACKET_PROXY_ACCESS_TOKEN` + - **説明**: PacketProxyのアクセストークン - **必須**: 推奨 (手動指定の代替) - **形式**: 文字列 @@ -640,12 +641,13 @@ MCP HTTP Bridgeは以下の環境変数をサポートします: - **動作**: 設定時、すべてのMCPツール呼び出しに自動的にアクセストークンが追加されます #### `MCP_DEBUG` + - **説明**: デバッグログ出力制御 - **必須**: オプション - **形式**: `"true"` または `"false"` - **デフォルト**: `"false"` - **例**: `export MCP_DEBUG="true"` -- **動作**: +- **動作**: - `"true"`: デバッグメッセージをstderrに出力 - `"false"`: デバッグメッセージを出力しない (JSON-RPC通信を汚染しない) From 80fd48883a725ebc79e0e6ae9730f9a7fe18d196 Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 4 Aug 2025 22:48:16 +0900 Subject: [PATCH 15/39] =?UTF-8?q?=E5=86=8D=E9=80=81=E6=99=82=E3=81=AE?= =?UTF-8?q?=E3=82=B3=E3=82=B9=E3=83=88=E3=82=92=E4=BD=8E=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/mcp/tools/ResendPacketTool.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java index 4d2bfc56..e264e26a 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java @@ -179,11 +179,11 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception try { ResendController resendController = ResendController.getInstance(); - if (count == 1 && modifications.size() == 0) { - // 単純な1回再送、改変なし - log("ResendPacketTool: Simple single resend without modifications"); - resendController.resend(originalOneShot); - sentCount = 1; + if (modifications.size() == 0) { + // 改変なしの場合は単純再送 + log("ResendPacketTool: Simple resend without modifications, count=" + count); + resendController.resend(originalOneShot, count); + sentCount = count; } else { // 複数回送信または改変ありの場合 log("ResendPacketTool: Complex resend with count=" + count + " and modifications=" From ceed271c1fe1863e9c638ce7d4fb29e8fc6ee360 Mon Sep 17 00:00:00 2001 From: kakira Date: Tue, 5 Aug 2025 00:02:24 +0900 Subject: [PATCH 16/39] =?UTF-8?q?=E3=83=84=E3=83=BC=E3=83=AB=E5=AE=9A?= =?UTF-8?q?=E7=BE=A9=E9=A0=86=E3=82=92=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-spec.md | 206 +++++++++--------- scripts/mcp_server.py | 94 ++++++-- .../extensions/mcp/tools/ToolRegistry.java | 2 +- 3 files changed, 178 insertions(+), 124 deletions(-) diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index ebf1dac5..e4e9da4c 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -178,7 +178,60 @@ PacketProxyのパケット履歴を検索・取得します。フィルタリン } ``` -### 3. `get_config` - 設定情報取得 +### 3. `get_logs` - ログ取得 + +PacketProxyのログを取得します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_logs", + "arguments": { + "access_token": "your_access_token_here", + "level": "info", + "limit": 100, + "since": "2025-01-15T00:00:00Z", + "filter": "error|exception" + } + }, + "id": 3 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `level` (string, optional): ログレベル "debug" | "info" | "warn" | "error" +- `limit` (number, optional): 取得件数 (デフォルト: 100) +- `since` (string, optional): 開始時刻 (ISO 8601形式) +- `filter` (string, optional): 正規表現フィルタ + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "logs": [ + { + "timestamp": "2025-01-15T10:30:00Z", + "level": "info", + "message": "PacketProxy started successfully", + "thread": "main", + "class": "packetproxy.PacketProxy" + } + ], + "total_count": 1500, + "has_more": true + }, + "id": 3 +} +``` + +### 4. `get_config` - 設定情報取得 PacketProxyの設定情報をHTTP API (`http://localhost:32349/config`) 経由で取得します。PacketProxyHub互換の完全な設定形式で返されます。 @@ -195,7 +248,7 @@ PacketProxyの設定情報をHTTP API (`http://localhost:32349/config`) 経由 "access_token": "your_access_token_here" } }, - "id": 3 + "id": 4 } ``` @@ -220,11 +273,11 @@ PacketProxyの設定情報をHTTP API (`http://localhost:32349/config`) 経由 } ] }, - "id": 3 + "id": 4 } ``` -### 4. `update_config` - 設定変更 +### 5. `update_config` - 設定変更 PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変更します。PacketProxyHub互換の形式を使用し、指定されたIDが含まれない項目は自動的に削除されます。 @@ -286,7 +339,7 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 "access_token": "your_access_token_here" } }, - "id": 4 + "id": 5 } ``` @@ -313,11 +366,53 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 } ] }, - "id": 4 + "id": 5 +} +``` + +### 6. `restore_config` - 設定バックアップ復元 + +指定したバックアップから設定を復元します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "restore_config", + "arguments": { + "access_token": "your_access_token_here", + "backup_id": "backup_20250115_103000" + } + }, + "id": 6 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `backup_id` (string, required): 復元するバックアップID + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "content": [ + { + "type": "text", + "text": "{\"success\": true, \"backup_id_restored\": \"backup_20250115_103000\", \"config_restored\": true}" + } + ] + }, + "id": 6 } ``` -### 5. `resend_packet` - パケット再送 +### 7. `resend_packet` - パケット再送 パケットを再送します。パケット改変や連続送信に対応しています。 @@ -350,7 +445,7 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 "allow_duplicate_headers": false } }, - "id": 5 + "id": 7 } ``` @@ -392,101 +487,6 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 "packet_ids": [124, 125, 126], "execution_time_ms": 2100 }, - "id": 5 -} -``` - -### 6. `get_logs` - ログ取得 - -PacketProxyのログを取得します。 - -**リクエスト:** - -```json -{ - "jsonrpc": "2.0", - "method": "tools/call", - "params": { - "name": "get_logs", - "arguments": { - "access_token": "your_access_token_here", - "level": "info", - "limit": 100, - "since": "2025-01-15T00:00:00Z", - "filter": "error|exception" - } - }, - "id": 6 -} -``` - -**パラメータ:** -- `access_token` (string, required): PacketProxy設定のアクセストークン -- `level` (string, optional): ログレベル "debug" | "info" | "warn" | "error" -- `limit` (number, optional): 取得件数 (デフォルト: 100) -- `since` (string, optional): 開始時刻 (ISO 8601形式) -- `filter` (string, optional): 正規表現フィルタ - -**レスポンス:** - -```json -{ - "jsonrpc": "2.0", - "result": { - "logs": [ - { - "timestamp": "2025-01-15T10:30:00Z", - "level": "info", - "message": "PacketProxy started successfully", - "thread": "main", - "class": "packetproxy.PacketProxy" - } - ], - "total_count": 1500, - "has_more": true - }, - "id": 6 -} -``` - -### 7. `restore_config` - 設定バックアップ復元 - -指定したバックアップから設定を復元します。 - -**リクエスト:** - -```json -{ - "jsonrpc": "2.0", - "method": "tools/call", - "params": { - "name": "restore_config", - "arguments": { - "access_token": "your_access_token_here", - "backup_id": "backup_20250115_103000" - } - }, - "id": 7 -} -``` - -**パラメータ:** -- `access_token` (string, required): PacketProxy設定のアクセストークン -- `backup_id` (string, required): 復元するバックアップID - -**レスポンス:** - -```json -{ - "jsonrpc": "2.0", - "result": { - "content": [ - { - "type": "text", - "text": "{\"success\": true, \"backup_id_restored\": \"backup_20250115_103000\", \"config_restored\": true}" - } - ] - }, "id": 7 } ``` diff --git a/scripts/mcp_server.py b/scripts/mcp_server.py index ee29a196..f6e9647a 100755 --- a/scripts/mcp_server.py +++ b/scripts/mcp_server.py @@ -72,6 +72,59 @@ def get_history(limit: Optional[int] = 100, offset: Optional[int] = 0) -> Dict[s ] } +@mcp.tool() +def get_packet_detail(packet_id: int, include_body: Optional[bool] = False) -> Dict[str, Any]: + """ + Get detailed information for a specific packet + + Args: + packet_id: ID of the packet to retrieve + include_body: Whether to include packet body content + + Returns: + Dictionary containing detailed packet information + """ + log_debug(f"get_packet_detail called with packet_id={packet_id}, include_body={include_body}") + + return { + "status": "connected", + "message": "PacketProxy MCP server is working. To get real packet details, ensure PacketProxy GUI is running with MCP Server extension enabled.", + "packet_id": packet_id, + "include_body": include_body, + "packet": { + "id": packet_id, + "method": "GET", + "url": "https://example.com/api/test", + "headers": { + "User-Agent": "Mozilla/5.0", + "Accept": "application/json" + }, + "body": "Example response body" if include_body else None + } + } + +@mcp.tool() +def get_logs() -> Dict[str, Any]: + """ + Get logs from PacketProxy + + Returns: + Dictionary containing log data + """ + log_debug("get_logs called") + + return { + "status": "connected", + "message": "PacketProxy MCP server is working. To get real logs, ensure PacketProxy GUI is running with MCP Server extension enabled.", + "logs": [ + { + "timestamp": "2025-08-02T06:30:00Z", + "level": "INFO", + "message": "PacketProxy started successfully" + } + ] + } + @mcp.tool() def get_configs(categories: Optional[List[str]] = None) -> Dict[str, Any]: """ @@ -100,34 +153,35 @@ def get_configs(categories: Optional[List[str]] = None) -> Dict[str, Any]: } @mcp.tool() -def get_packet_detail(packet_id: int, include_body: Optional[bool] = False) -> Dict[str, Any]: +def update_config() -> Dict[str, Any]: """ - Get detailed information for a specific packet + Update PacketProxy configuration settings - Args: - packet_id: ID of the packet to retrieve - include_body: Whether to include packet body content + Returns: + Dictionary containing update results + """ + log_debug("update_config called") + + return { + "status": "connected", + "message": "PacketProxy MCP server is working. To update configuration, ensure PacketProxy GUI is running with MCP Server extension enabled.", + "success": True + } + +@mcp.tool() +def restore_config() -> Dict[str, Any]: + """ + Restore PacketProxy configuration from backup Returns: - Dictionary containing detailed packet information + Dictionary containing restore results """ - log_debug(f"get_packet_detail called with packet_id={packet_id}, include_body={include_body}") + log_debug("restore_config called") return { "status": "connected", - "message": "PacketProxy MCP server is working. To get real packet details, ensure PacketProxy GUI is running with MCP Server extension enabled.", - "packet_id": packet_id, - "include_body": include_body, - "packet": { - "id": packet_id, - "method": "GET", - "url": "https://example.com/api/test", - "headers": { - "User-Agent": "Mozilla/5.0", - "Accept": "application/json" - }, - "body": "Example response body" if include_body else None - } + "message": "PacketProxy MCP server is working. To restore configuration, ensure PacketProxy GUI is running with MCP Server extension enabled.", + "success": True } @mcp.tool() diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java index ca8e6723..5333c63d 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java @@ -18,10 +18,10 @@ private void registerDefaultTools() { // 基本的なツールを登録 registerTool(new HistoryTool()); registerTool(new PacketDetailTool()); + registerTool(new LogTool()); registerTool(new ConfigTool()); registerTool(new UpdateConfigTool()); registerTool(new RestoreConfigTool()); - registerTool(new LogTool()); registerTool(new ResendPacketTool()); } From 901334a5513512625e06fe2548104881a9b9dcf2 Mon Sep 17 00:00:00 2001 From: kakira Date: Tue, 5 Aug 2025 00:07:00 +0900 Subject: [PATCH 17/39] =?UTF-8?q?PACKETPROXY=5FACCESS=5FTOKEN=20=E3=81=AB?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-setting-guide.md | 4 ++-- doc/mcp-server-spec.md | 12 ++++++------ scripts/mcp-http-bridge.js | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/mcp-server-setting-guide.md b/doc/mcp-server-setting-guide.md index 7ea0148f..af6edcb7 100644 --- a/doc/mcp-server-setting-guide.md +++ b/doc/mcp-server-setting-guide.md @@ -65,7 +65,7 @@ open ~/Library/Application\ Support/Claude/claude_desktop_config.json "/Users/kakira/PacketProxy/scripts/mcp-http-bridge.js" ], "env": { - "PACKET_PROXY_ACCESS_TOKEN": "your_access_token_here" + "PACKETPROXY_ACCESS_TOKEN": "your_access_token_here" } } } @@ -87,7 +87,7 @@ open ~/Library/Application\ Support/Claude/claude_desktop_config.json "/Users/kakira/PacketProxy/scripts/mcp-http-bridge.js" ], "env": { - "PACKET_PROXY_ACCESS_TOKEN": "your_access_token_here" + "PACKETPROXY_ACCESS_TOKEN": "your_access_token_here" } } } diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index e4e9da4c..69ee0356 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -43,7 +43,7 @@ PacketProxy MCP サーバーは、Model Context Protocol (MCP) を使用してPa MCP HTTP Bridgeを使用する場合、環境変数にアクセストークンを設定することで、自動的に認証情報が追加されます: ```bash -export PACKET_PROXY_ACCESS_TOKEN="your_access_token_here" +export PACKETPROXY_ACCESS_TOKEN="your_access_token_here" ``` この設定により、各ツール呼び出し時に手動で`access_token`パラメータを指定する必要がなくなります。 @@ -53,7 +53,7 @@ export PACKET_PROXY_ACCESS_TOKEN="your_access_token_here" - アクセストークンが未設定: PacketProxyでconfig sharingを有効にしてください - アクセストークンが無効: Settings画面で正しいトークンを確認してください - アクセストークンが空: 必須パラメータのため、必ず指定してください -- 環境変数が設定されていない場合: `PACKET_PROXY_ACCESS_TOKEN`環境変数を確認してください +- 環境変数が設定されていない場合: `PACKETPROXY_ACCESS_TOKEN`環境変数を確認してください ## MCPツール一覧 @@ -632,12 +632,12 @@ Authorization: Bearer MCP HTTP Bridgeは以下の環境変数をサポートします: -#### `PACKET_PROXY_ACCESS_TOKEN` +#### `PACKETPROXY_ACCESS_TOKEN` - **説明**: PacketProxyのアクセストークン - **必須**: 推奨 (手動指定の代替) - **形式**: 文字列 -- **例**: `export PACKET_PROXY_ACCESS_TOKEN="abc123def456"` +- **例**: `export PACKETPROXY_ACCESS_TOKEN="abc123def456"` - **動作**: 設定時、すべてのMCPツール呼び出しに自動的にアクセストークンが追加されます #### `MCP_DEBUG` @@ -655,11 +655,11 @@ MCP HTTP Bridgeは以下の環境変数をサポートします: ```bash # 基本設定 -export PACKET_PROXY_ACCESS_TOKEN="your_access_token_here" +export PACKETPROXY_ACCESS_TOKEN="your_access_token_here" # デバッグ有効化 export MCP_DEBUG="true" -export PACKET_PROXY_ACCESS_TOKEN="your_access_token_here" +export PACKETPROXY_ACCESS_TOKEN="your_access_token_here" # MCP HTTP Bridge起動 node /path/to/mcp-http-bridge.js diff --git a/scripts/mcp-http-bridge.js b/scripts/mcp-http-bridge.js index 28a9f6ee..6ec7ff53 100755 --- a/scripts/mcp-http-bridge.js +++ b/scripts/mcp-http-bridge.js @@ -196,8 +196,8 @@ class MCPHttpBridge { async forwardToPacketProxy(request) { return new Promise((resolve, reject) => { // 環境変数からアクセストークンを取得 - const accessToken = process.env.PACKET_PROXY_ACCESS_TOKEN; - debugLog(`[DEBUG] Environment variable PACKET_PROXY_ACCESS_TOKEN: ${accessToken ? '[SET]' : '[NOT SET]'}`); + const accessToken = process.env.PACKETPROXY_ACCESS_TOKEN; + debugLog(`[DEBUG] Environment variable PACKETPROXY_ACCESS_TOKEN: ${accessToken ? '[SET]' : '[NOT SET]'}`); debugLog(`[DEBUG] Request method: ${request.method}`); debugLog(`[DEBUG] Request params: ${JSON.stringify(request.params)}`); @@ -218,7 +218,7 @@ class MCPHttpBridge { debugLog(`[DEBUG] Added access token to request params`); } } else if (!accessToken) { - debugLog(`[WARNING] PACKET_PROXY_ACCESS_TOKEN environment variable not set`); + debugLog(`[WARNING] PACKETPROXY_ACCESS_TOKEN environment variable not set`); } const postData = JSON.stringify(request); From eccaa4b13e3ae594061e1f6bf1371573dd88f111 Mon Sep 17 00:00:00 2001 From: kakira Date: Tue, 5 Aug 2025 00:14:00 +0900 Subject: [PATCH 18/39] =?UTF-8?q?=E7=8F=BE=E5=9C=A8=E3=81=AF=E4=BD=BF?= =?UTF-8?q?=E3=81=A3=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E3=82=B9=E3=82=AF?= =?UTF-8?q?=E3=83=AA=E3=83=97=E3=83=88=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/mcp-server.sh | 19 ---- scripts/mcp_server.py | 237 ------------------------------------------ 2 files changed, 256 deletions(-) delete mode 100755 scripts/mcp-server.sh delete mode 100755 scripts/mcp_server.py diff --git a/scripts/mcp-server.sh b/scripts/mcp-server.sh deleted file mode 100755 index 80e5d242..00000000 --- a/scripts/mcp-server.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# PacketProxy MCP Server launcher script -# This script starts PacketProxy in headless mode with MCP server enabled - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PACKETPROXY_DIR="$(dirname "$SCRIPT_DIR")" -JAR_FILE="$PACKETPROXY_DIR/build/libs/PacketProxy.jar" - -# Check if PacketProxy jar exists -if [ ! -f "$JAR_FILE" ]; then - echo "Error: PacketProxy.jar not found at $JAR_FILE" >&2 - echo "Please run 'gradlew build' first" >&2 - exit 1 -fi - -# Start PacketProxy with MCP server in headless mode -# We need to start PacketProxy GUI and enable MCP server programmatically -java -jar "$JAR_FILE" --mcp-server-mode diff --git a/scripts/mcp_server.py b/scripts/mcp_server.py deleted file mode 100755 index f6e9647a..00000000 --- a/scripts/mcp_server.py +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env python3 -""" -PacketProxy MCP Server -Provides MCP tools for interacting with PacketProxy -""" - -import sys -import os -import subprocess -import json -from pathlib import Path -from typing import Optional, List, Dict, Any - -# Add stderr logging for debugging -def log_debug(message: str): - print(f"DEBUG: {message}", file=sys.stderr, flush=True) - -try: - from mcp.server.fastmcp import FastMCP - log_debug("MCP SDK imported successfully") -except ImportError as e: - log_debug(f"Failed to import MCP SDK: {e}") - sys.exit(1) - -# Create MCP server -mcp = FastMCP("PacketProxy MCP Server") -log_debug("FastMCP server created") - -# Get PacketProxy directory -script_dir = Path(__file__).parent -packetproxy_dir = script_dir.parent -jar_file = packetproxy_dir / "build" / "libs" / "PacketProxy.jar" - -log_debug(f"PacketProxy directory: {packetproxy_dir}") -log_debug(f"JAR file path: {jar_file}") - -if not jar_file.exists(): - log_debug(f"Error: PacketProxy.jar not found at {jar_file}") - log_debug("Please run './gradlew build' first") - sys.exit(1) - -log_debug("PacketProxy.jar found") - -@mcp.tool() -def get_history(limit: Optional[int] = 100, offset: Optional[int] = 0) -> Dict[str, Any]: - """ - Get packet history from PacketProxy - - Args: - limit: Maximum number of packets to return (default: 100) - offset: Number of packets to skip (default: 0) - - Returns: - Dictionary containing packet history data - """ - log_debug(f"get_history called with limit={limit}, offset={offset}") - - # For now, return mock data indicating connection is working - return { - "status": "connected", - "message": "PacketProxy MCP server is working. To get real packet data, ensure PacketProxy GUI is running with MCP Server extension enabled.", - "limit": limit, - "offset": offset, - "packets": [ - { - "id": 1, - "method": "GET", - "url": "https://example.com/api/test", - "status": 200, - "timestamp": "2025-08-02T06:30:00Z" - } - ] - } - -@mcp.tool() -def get_packet_detail(packet_id: int, include_body: Optional[bool] = False) -> Dict[str, Any]: - """ - Get detailed information for a specific packet - - Args: - packet_id: ID of the packet to retrieve - include_body: Whether to include packet body content - - Returns: - Dictionary containing detailed packet information - """ - log_debug(f"get_packet_detail called with packet_id={packet_id}, include_body={include_body}") - - return { - "status": "connected", - "message": "PacketProxy MCP server is working. To get real packet details, ensure PacketProxy GUI is running with MCP Server extension enabled.", - "packet_id": packet_id, - "include_body": include_body, - "packet": { - "id": packet_id, - "method": "GET", - "url": "https://example.com/api/test", - "headers": { - "User-Agent": "Mozilla/5.0", - "Accept": "application/json" - }, - "body": "Example response body" if include_body else None - } - } - -@mcp.tool() -def get_logs() -> Dict[str, Any]: - """ - Get logs from PacketProxy - - Returns: - Dictionary containing log data - """ - log_debug("get_logs called") - - return { - "status": "connected", - "message": "PacketProxy MCP server is working. To get real logs, ensure PacketProxy GUI is running with MCP Server extension enabled.", - "logs": [ - { - "timestamp": "2025-08-02T06:30:00Z", - "level": "INFO", - "message": "PacketProxy started successfully" - } - ] - } - -@mcp.tool() -def get_configs(categories: Optional[List[str]] = None) -> Dict[str, Any]: - """ - Get PacketProxy configuration settings - - Args: - categories: List of configuration categories to retrieve - - Returns: - Dictionary containing configuration data - """ - log_debug(f"get_configs called with categories={categories}") - - return { - "status": "connected", - "message": "PacketProxy MCP server is working. To get real configuration data, ensure PacketProxy GUI is running with MCP Server extension enabled.", - "categories": categories or ["all"], - "configs": { - "listenPorts": [ - {"port": 8080, "protocol": "HTTP"} - ], - "servers": [ - {"name": "test-server", "host": "localhost", "port": 3000} - ] - } - } - -@mcp.tool() -def update_config() -> Dict[str, Any]: - """ - Update PacketProxy configuration settings - - Returns: - Dictionary containing update results - """ - log_debug("update_config called") - - return { - "status": "connected", - "message": "PacketProxy MCP server is working. To update configuration, ensure PacketProxy GUI is running with MCP Server extension enabled.", - "success": True - } - -@mcp.tool() -def restore_config() -> Dict[str, Any]: - """ - Restore PacketProxy configuration from backup - - Returns: - Dictionary containing restore results - """ - log_debug("restore_config called") - - return { - "status": "connected", - "message": "PacketProxy MCP server is working. To restore configuration, ensure PacketProxy GUI is running with MCP Server extension enabled.", - "success": True - } - -@mcp.tool() -def resend_packet( - packet_id: int, - access_token: str, - count: Optional[int] = 1, - interval_ms: Optional[int] = 0, - modifications: Optional[List[Dict[str, Any]]] = None, - async_mode: Optional[bool] = False, - allow_duplicate_headers: Optional[bool] = False -) -> Dict[str, Any]: - """ - Resend a packet with optional modifications and multiple count support - - Args: - packet_id: ID of the packet to resend - access_token: Access token for authentication - count: Number of times to send the packet (default: 1) - interval_ms: Interval between sends in milliseconds (default: 0) - modifications: Array of modification rules to apply to the packet - async_mode: Execute asynchronously (default: false) - allow_duplicate_headers: Allow duplicate headers when adding/modifying headers (default: false) - - Returns: - Dictionary containing resend operation results - """ - log_debug(f"resend_packet called with packet_id={packet_id}, count={count}, interval_ms={interval_ms}, async={async_mode}, allow_duplicate_headers={allow_duplicate_headers}") - - return { - "status": "connected", - "message": "PacketProxy MCP server is working. To resend packets, ensure PacketProxy GUI is running with MCP Server extension enabled.", - "success": True, - "sent_count": count, - "failed_count": 0, - "packet_ids": [packet_id + i for i in range(1, count + 1)], - "execution_time_ms": count * (interval_ms + 10) # simulated execution time - } - -def main(): - log_debug("Starting PacketProxy MCP Server...") - - # Run the MCP server - try: - log_debug("About to call mcp.run()") - mcp.run() - log_debug("mcp.run() completed") - except Exception as e: - log_debug(f"Error running MCP server: {e}") - raise - -if __name__ == "__main__": - main() \ No newline at end of file From f18b3cb6e2b33498ca9a29e91f77e79218d76ed2 Mon Sep 17 00:00:00 2001 From: kakira Date: Tue, 5 Aug 2025 00:26:44 +0900 Subject: [PATCH 19/39] WIP: bulk_sender --- doc/mcp-server-spec.md | 135 ++++++ .../extensions/mcp/tools/BulkSendTool.java | 404 ++++++++++++++++++ .../extensions/mcp/tools/ToolRegistry.java | 1 + 3 files changed, 540 insertions(+) create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index 69ee0356..fab2c6fb 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -491,6 +491,140 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 } ``` +### 8. `bulk_send` - 複数パケット一括送信 + +複数のパケットを一括で送信します。並列・順次送信モード、動的パラメータ、改変機能をサポートします。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "bulk_send", + "arguments": { + "access_token": "your_access_token_here", + "packet_ids": [123, 124, 125], + "mode": "sequential", + "count": 2, + "interval_ms": 500, + "modifications": [ + { + "type": "header_add", + "name": "X-Test-Run", + "value": "{{timestamp}}" + } + ], + "regex_params": [ + { + "pattern": "token=([a-zA-Z0-9]+)", + "value_template": "token={{random}}-{{packet_index}}", + "target": "request" + } + ], + "allow_duplicate_headers": false, + "async": false, + "timeout_ms": 30000 + } + }, + "id": 8 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `packet_ids` (array, required): 送信するパケットIDの配列 (1-100個) +- `mode` (string, optional): 送信モード "parallel" | "sequential" (デフォルト: "parallel") +- `count` (number, optional): 各パケットの送信回数 (デフォルト: 1, 最大: 1000) +- `interval_ms` (number, optional): 送信間隔(ms) (順次モードのみ, デフォルト: 0, 最大: 60000) +- `modifications` (array, optional): 全パケットに適用する改変設定 (resend_packetと同じ形式) +- `regex_params` (array, optional): 動的値置換パラメータ +- `allow_duplicate_headers` (boolean, optional): ヘッダー重複許可 (デフォルト: false) +- `async` (boolean, optional): 非同期実行 (デフォルト: false) +- `timeout_ms` (number, optional): 全体タイムアウト(ms) (デフォルト: 30000, 最大: 300000) + +**regex_params設定:** +- `packet_index` (number, optional): 対象パケットインデックス (0ベース、省略時は全パケット) +- `pattern` (string, required): マッチする正規表現パターン +- `value_template` (string, required): 置換テンプレート (変数: {{packet_index}}, {{timestamp}}, {{random}}, {{uuid}}) +- `target` (string, optional): 対象 "request" | "response" | "both" (デフォルト: "request") + +**送信モード:** +- `parallel`: 全パケットを並列送信 (高速、interval_msは無視) +- `sequential`: パケットを順次送信 (制御された実行、regex_paramsによる値の引き継ぎ) + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "success": true, + "mode": "sequential", + "total_packets": 3, + "total_count": 6, + "sent_count": 5, + "failed_count": 1, + "execution_time_ms": 1250, + "results": [ + { + "original_packet_id": 123, + "packet_index": 0, + "success": true, + "sent_count": 2, + "failed_count": 0, + "new_packet_ids": [145, 146], + "error": null, + "execution_time_ms": 245 + }, + { + "original_packet_id": 124, + "packet_index": 1, + "success": false, + "sent_count": 0, + "failed_count": 2, + "new_packet_ids": [], + "error": "Connection timeout", + "execution_time_ms": 5000 + } + ], + "regex_params_applied": [ + { + "packet_index": 0, + "pattern": "token=([a-zA-Z0-9]+)", + "extracted_value": "abc123def", + "applied_count": 1 + } + ], + "performance": { + "packets_per_second": 4.0, + "average_response_time_ms": 312, + "concurrent_connections": 3 + }, + "job_id": null + }, + "id": 8 +} +``` + +**非同期実行レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "async": true, + "job_id": "bulk_send_20250804_120030_abc123", + "status": "started", + "total_packets": 50, + "estimated_duration_ms": 30000, + "monitor_url": "/mcp/bulk_send/status/bulk_send_20250804_120030_abc123" + }, + "id": 8 +} +``` + ## フィルタ構文仕様 PacketProxyのFilterTextParserに準拠した構文を使用します。 @@ -571,6 +705,7 @@ GET /mcp/packet/{id} # パケット詳細 GET /mcp/configs # 設定一覧 PUT /mcp/configs # 設定更新 POST /mcp/resend/{packet_id} # パケット再送 +POST /mcp/bulk_send # 複数パケット一括送信 GET /mcp/logs?level=info # ログ取得 POST /mcp/restore/{backup_id} # バックアップ復元 ``` diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java new file mode 100644 index 00000000..3e86c1cf --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java @@ -0,0 +1,404 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import packetproxy.controller.ResendController; +import packetproxy.controller.ResendController.ResendWorker; +import packetproxy.model.OneShotPacket; +import packetproxy.model.Packet; +import packetproxy.model.Packets; + +/** + * 複数パケット一括送信ツール + * フェーズ1: 基本的な並列送信機能とmodifications適用 + */ +public class BulkSendTool extends AuthenticatedMCPTool { + + @Override + public String getName() { + return "bulk_send"; + } + + @Override + public String getDescription() { + return "Send multiple packets in bulk with optional modifications and multiple count support"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + // packet_ids (required) + JsonObject packetIdsProp = new JsonObject(); + packetIdsProp.addProperty("type", "array"); + packetIdsProp.addProperty("description", "Array of packet IDs to send (1-100 packets)"); + JsonObject packetIdsItems = new JsonObject(); + packetIdsItems.addProperty("type", "integer"); + packetIdsProp.add("items", packetIdsItems); + schema.add("packet_ids", packetIdsProp); + + // mode (optional) + JsonObject modeProp = new JsonObject(); + modeProp.addProperty("type", "string"); + JsonArray modeEnum = new JsonArray(); + modeEnum.add("parallel"); + modeEnum.add("sequential"); + modeProp.add("enum", modeEnum); + modeProp.addProperty("description", "Sending mode: parallel (fast) or sequential (controlled) - Phase 1 supports parallel only"); + modeProp.addProperty("default", "parallel"); + schema.add("mode", modeProp); + + // count (optional) + JsonObject countProp = new JsonObject(); + countProp.addProperty("type", "integer"); + countProp.addProperty("description", "Number of times to send each packet (default: 1)"); + countProp.addProperty("default", 1); + countProp.addProperty("minimum", 1); + countProp.addProperty("maximum", 1000); + schema.add("count", countProp); + + // modifications (optional) - ResendPacketToolと同じ形式 + JsonObject modificationsProp = new JsonObject(); + modificationsProp.addProperty("type", "array"); + modificationsProp.addProperty("description", "Array of modification rules to apply to all packets"); + JsonObject modificationItem = new JsonObject(); + modificationItem.addProperty("type", "object"); + JsonObject modificationProps = new JsonObject(); + + JsonObject targetProp = new JsonObject(); + targetProp.addProperty("type", "string"); + JsonArray targetEnum = new JsonArray(); + targetEnum.add("request"); + targetEnum.add("response"); + targetEnum.add("both"); + targetProp.add("enum", targetEnum); + targetProp.addProperty("description", "Target to modify: request, response, or both"); + modificationProps.add("target", targetProp); + + JsonObject typeProp = new JsonObject(); + typeProp.addProperty("type", "string"); + JsonArray typeEnum = new JsonArray(); + typeEnum.add("regex_replace"); + typeEnum.add("header_add"); + typeEnum.add("header_modify"); + typeProp.add("enum", typeEnum); + typeProp.addProperty("description", "Type of modification"); + modificationProps.add("type", typeProp); + + JsonObject patternProp = new JsonObject(); + patternProp.addProperty("type", "string"); + patternProp.addProperty("description", "Regex pattern for regex_replace type"); + modificationProps.add("pattern", patternProp); + + JsonObject replacementProp = new JsonObject(); + replacementProp.addProperty("type", "string"); + replacementProp.addProperty("description", "Replacement string for regex_replace or value for headers"); + modificationProps.add("replacement", replacementProp); + + JsonObject nameProp = new JsonObject(); + nameProp.addProperty("type", "string"); + nameProp.addProperty("description", "Header name for header_add/header_modify"); + modificationProps.add("name", nameProp); + + JsonObject valueProp = new JsonObject(); + valueProp.addProperty("type", "string"); + valueProp.addProperty("description", "Header value for header_add/header_modify"); + modificationProps.add("value", valueProp); + + modificationItem.add("properties", modificationProps); + modificationsProp.add("items", modificationItem); + schema.add("modifications", modificationsProp); + + // allow_duplicate_headers (optional) + JsonObject allowDuplicateHeadersProp = new JsonObject(); + allowDuplicateHeadersProp.addProperty("type", "boolean"); + allowDuplicateHeadersProp.addProperty("description", + "Allow duplicate headers when adding/modifying headers (default: false - replace existing headers)"); + allowDuplicateHeadersProp.addProperty("default", false); + schema.add("allow_duplicate_headers", allowDuplicateHeadersProp); + + // timeout_ms (optional) + JsonObject timeoutProp = new JsonObject(); + timeoutProp.addProperty("type", "integer"); + timeoutProp.addProperty("description", "Timeout for entire bulk operation in milliseconds (default: 30000)"); + timeoutProp.addProperty("default", 30000); + timeoutProp.addProperty("minimum", 1000); + timeoutProp.addProperty("maximum", 300000); + schema.add("timeout_ms", timeoutProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("BulkSendTool: Starting bulk send operation"); + + // パラメータ取得 + if (!arguments.has("packet_ids")) { + throw new IllegalArgumentException("packet_ids parameter is required"); + } + + JsonArray packetIdsArray = arguments.getAsJsonArray("packet_ids"); + if (packetIdsArray.size() == 0) { + throw new IllegalArgumentException("packet_ids array cannot be empty"); + } + if (packetIdsArray.size() > 100) { + throw new IllegalArgumentException("packet_ids array cannot exceed 100 packets"); + } + + String mode = arguments.has("mode") ? arguments.get("mode").getAsString() : "parallel"; + int count = arguments.has("count") ? arguments.get("count").getAsInt() : 1; + boolean allowDuplicateHeaders = arguments.has("allow_duplicate_headers") + ? arguments.get("allow_duplicate_headers").getAsBoolean() + : false; + int timeoutMs = arguments.has("timeout_ms") ? arguments.get("timeout_ms").getAsInt() : 30000; + + JsonArray modifications = arguments.has("modifications") + ? arguments.getAsJsonArray("modifications") + : new JsonArray(); + + // フェーズ1では並列送信のみサポート + if (!"parallel".equals(mode)) { + throw new IllegalArgumentException("Phase 1 supports parallel mode only. Sequential mode will be available in Phase 2."); + } + + log("BulkSendTool: packet_ids=" + packetIdsArray.size() + ", mode=" + mode + ", count=" + count + + ", allowDuplicateHeaders=" + allowDuplicateHeaders + ", timeout=" + timeoutMs + "ms"); + + // パケットIDを取得 + List packetIds = new ArrayList<>(); + for (JsonElement element : packetIdsArray) { + packetIds.add(element.getAsInt()); + } + + long startTime = System.currentTimeMillis(); + int totalPackets = packetIds.size(); + int totalCount = totalPackets * count; + int sentCount = 0; + int failedCount = 0; + List results = new ArrayList<>(); + + try { + // 各パケットを処理 + for (int i = 0; i < packetIds.size(); i++) { + int packetId = packetIds.get(i); + BulkSendResult result = processSinglePacket(packetId, i, count, modifications, allowDuplicateHeaders); + results.add(result); + sentCount += result.sentCount; + failedCount += result.failedCount; + } + + } catch (Exception e) { + log("BulkSendTool: Bulk send operation failed: " + e.getMessage()); + throw e; + } + + long executionTime = System.currentTimeMillis() - startTime; + + // 結果作成 + JsonObject result = new JsonObject(); + result.addProperty("success", failedCount == 0); + result.addProperty("mode", mode); + result.addProperty("total_packets", totalPackets); + result.addProperty("total_count", totalCount); + result.addProperty("sent_count", sentCount); + result.addProperty("failed_count", failedCount); + result.addProperty("execution_time_ms", executionTime); + + // 詳細結果 + JsonArray resultsArray = new JsonArray(); + for (BulkSendResult r : results) { + JsonObject resultObj = new JsonObject(); + resultObj.addProperty("original_packet_id", r.originalPacketId); + resultObj.addProperty("packet_index", r.packetIndex); + resultObj.addProperty("success", r.success); + resultObj.addProperty("sent_count", r.sentCount); + resultObj.addProperty("failed_count", r.failedCount); + + JsonArray newPacketIds = new JsonArray(); + for (Integer id : r.newPacketIds) { + newPacketIds.add(id); + } + resultObj.add("new_packet_ids", newPacketIds); + + if (r.error != null) { + resultObj.addProperty("error", r.error); + } + resultObj.addProperty("execution_time_ms", r.executionTimeMs); + + resultsArray.add(resultObj); + } + result.add("results", resultsArray); + + // パフォーマンス統計 + JsonObject performance = new JsonObject(); + double packetsPerSecond = totalCount > 0 ? (double) sentCount / (executionTime / 1000.0) : 0.0; + double avgResponseTime = results.size() > 0 + ? results.stream().mapToLong(r -> r.executionTimeMs).average().orElse(0.0) + : 0.0; + + performance.addProperty("packets_per_second", Math.round(packetsPerSecond * 100.0) / 100.0); + performance.addProperty("average_response_time_ms", Math.round(avgResponseTime)); + performance.addProperty("concurrent_connections", totalPackets); + result.add("performance", performance); + + result.add("job_id", null); // 非同期実行はフェーズ3で実装 + + log("BulkSendTool: Completed. Sent: " + sentCount + ", Failed: " + failedCount + ", Time: " + executionTime + "ms"); + return result; + } + + /** + * 単一パケットの処理(並列送信) + */ + private BulkSendResult processSinglePacket(int packetId, int packetIndex, int count, + JsonArray modifications, boolean allowDuplicateHeaders) { + + BulkSendResult result = new BulkSendResult(); + result.originalPacketId = packetId; + result.packetIndex = packetIndex; + result.newPacketIds = new ArrayList<>(); + + long startTime = System.currentTimeMillis(); + + try { + // パケットを取得 + Packet originalPacket = Packets.getInstance().query(packetId); + if (originalPacket == null) { + result.success = false; + result.failedCount = count; + result.error = "Packet with ID " + packetId + " not found"; + return result; + } + + // OneShotPacketを作成 + OneShotPacket originalOneShot = createOneShotPacket(originalPacket); + if (originalOneShot == null) { + result.success = false; + result.failedCount = count; + result.error = "Cannot create OneShotPacket from packet ID " + packetId; + return result; + } + + // 改変を適用 + OneShotPacket modifiedPacket = applyModifications(originalOneShot, modifications, packetIndex + 1, allowDuplicateHeaders); + + // 複数回送信用のパケット配列を作成 + OneShotPacket[] packetsToSend = new OneShotPacket[count]; + for (int i = 0; i < count; i++) { + packetsToSend[i] = modifiedPacket; + } + + // ResendControllerを使用して並列送信 + CountDownLatch latch = new CountDownLatch(1); + List receivedPackets = new ArrayList<>(); + List sendErrors = new ArrayList<>(); + + ResendController.getInstance().resend(new ResendWorker(packetsToSend) { + @Override + protected void process(List chunks) { + synchronized (receivedPackets) { + receivedPackets.addAll(chunks); + } + } + + @Override + protected void done() { + try { + get(); // 例外があれば取得 + } catch (Exception e) { + synchronized (sendErrors) { + sendErrors.add(e); + } + } + latch.countDown(); + } + }); + + // 完了を待機 + boolean completed = latch.await(30, TimeUnit.SECONDS); + if (!completed) { + result.success = false; + result.failedCount = count; + result.error = "Timeout waiting for packet sending completion"; + return result; + } + + // 結果を設定 + if (!sendErrors.isEmpty()) { + result.success = false; + result.failedCount = count; + result.error = "Send failed: " + sendErrors.get(0).getMessage(); + } else { + result.success = true; + result.sentCount = count; + // 新しいパケットIDは実際の実装では取得困難なため空のままとする + } + + } catch (Exception e) { + result.success = false; + result.failedCount = count; + result.error = e.getMessage(); + log("BulkSendTool: Failed to process packet " + packetId + ": " + e.getMessage()); + } finally { + result.executionTimeMs = System.currentTimeMillis() - startTime; + } + + return result; + } + + /** + * OneShotPacketを作成(ResendPacketToolと同じロジック) + */ + private OneShotPacket createOneShotPacket(Packet originalPacket) throws Exception { + if (originalPacket.getModifiedData().length > 0) { + return originalPacket.getOneShotFromModifiedData(); + } else if (originalPacket.getSentData().length > 0) { + return originalPacket.getOneShotPacket(originalPacket.getSentData()); + } else { + return originalPacket.getOneShotFromDecodedData(); + } + } + + /** + * パケットに改変を適用(ResendPacketToolのロジックを再利用) + */ + private OneShotPacket applyModifications(OneShotPacket original, JsonArray modifications, int index, + boolean allowDuplicateHeaders) throws Exception { + + // ResendPacketToolのapplyModificationsメソッドと同じ実装 + // ここでは簡略化のため、modificationsが空の場合はオリジナルを返す + if (modifications.size() == 0) { + return original; + } + + log("BulkSendTool: Applying " + modifications.size() + " modifications to packet (index=" + index + ")"); + + // 実際の改変処理はResendPacketToolと同じ実装を使用 + // フェーズ1では基本的な処理のみ実装 + // TODO: ResendPacketToolのapplyModificationsメソッドを共通化するか、ここで再実装 + + return original; // フェーズ1では改変なしで返す + } + + /** + * 個別パケットの送信結果 + */ + private static class BulkSendResult { + int originalPacketId; + int packetIndex; + boolean success; + int sentCount; + int failedCount; + List newPacketIds; + String error; + long executionTimeMs; + } +} \ No newline at end of file diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java index 5333c63d..04df5a21 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java @@ -23,6 +23,7 @@ private void registerDefaultTools() { registerTool(new UpdateConfigTool()); registerTool(new RestoreConfigTool()); registerTool(new ResendPacketTool()); + registerTool(new BulkSendTool()); } public void registerTool(MCPTool tool) { From c8411d9e092390b219be8e6574b5e06e14b4a938 Mon Sep 17 00:00:00 2001 From: kakira Date: Tue, 5 Aug 2025 01:11:48 +0900 Subject: [PATCH 20/39] WIP: bulk_sender phase 2 --- .../extensions/mcp/tools/BulkSendTool.java | 532 +++++++++++++++++- 1 file changed, 509 insertions(+), 23 deletions(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java index 3e86c1cf..2e1cc5ba 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java @@ -5,10 +5,19 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import packetproxy.controller.ResendController; import packetproxy.controller.ResendController.ResendWorker; import packetproxy.model.OneShotPacket; @@ -17,7 +26,7 @@ /** * 複数パケット一括送信ツール - * フェーズ1: 基本的な並列送信機能とmodifications適用 + * フェーズ2: 順次送信モード、modifications適用、regex_params機能 */ public class BulkSendTool extends AuthenticatedMCPTool { @@ -51,7 +60,7 @@ public JsonObject getInputSchema() { modeEnum.add("parallel"); modeEnum.add("sequential"); modeProp.add("enum", modeEnum); - modeProp.addProperty("description", "Sending mode: parallel (fast) or sequential (controlled) - Phase 1 supports parallel only"); + modeProp.addProperty("description", "Sending mode: parallel (fast) or sequential (controlled)"); modeProp.addProperty("default", "parallel"); schema.add("mode", modeProp); @@ -64,6 +73,53 @@ public JsonObject getInputSchema() { countProp.addProperty("maximum", 1000); schema.add("count", countProp); + // interval_ms (optional) + JsonObject intervalProp = new JsonObject(); + intervalProp.addProperty("type", "integer"); + intervalProp.addProperty("description", "Interval between sends in milliseconds (sequential mode only, default: 0, maximum: 60000)"); + intervalProp.addProperty("default", 0); + intervalProp.addProperty("minimum", 0); + intervalProp.addProperty("maximum", 60000); + schema.add("interval_ms", intervalProp); + + // regex_params (optional) + JsonObject regexParamsProp = new JsonObject(); + regexParamsProp.addProperty("type", "array"); + regexParamsProp.addProperty("description", "Regex parameters for dynamic value replacement across packets"); + JsonObject regexParamItem = new JsonObject(); + regexParamItem.addProperty("type", "object"); + JsonObject regexParamProps = new JsonObject(); + + JsonObject packetIndexProp = new JsonObject(); + packetIndexProp.addProperty("type", "integer"); + packetIndexProp.addProperty("description", "Target packet index (0-based)"); + regexParamProps.add("packet_index", packetIndexProp); + + JsonObject regexPatternProp = new JsonObject(); + regexPatternProp.addProperty("type", "string"); + regexPatternProp.addProperty("description", "Regex pattern to match"); + regexParamProps.add("pattern", regexPatternProp); + + JsonObject valueTemplateProp = new JsonObject(); + valueTemplateProp.addProperty("type", "string"); + valueTemplateProp.addProperty("description", "Template with variables: {{packet_index}}, {{timestamp}}, {{random}}, {{uuid}}"); + regexParamProps.add("value_template", valueTemplateProp); + + JsonObject regexTargetProp = new JsonObject(); + regexTargetProp.addProperty("type", "string"); + JsonArray regexTargetEnum = new JsonArray(); + regexTargetEnum.add("request"); + regexTargetEnum.add("response"); + regexTargetEnum.add("both"); + regexTargetProp.add("enum", regexTargetEnum); + regexTargetProp.addProperty("description", "Target: request, response, or both (default: request)"); + regexTargetProp.addProperty("default", "request"); + regexParamProps.add("target", regexTargetProp); + + regexParamItem.add("properties", regexParamProps); + regexParamsProp.add("items", regexParamItem); + schema.add("regex_params", regexParamsProp); + // modifications (optional) - ResendPacketToolと同じ形式 JsonObject modificationsProp = new JsonObject(); modificationsProp.addProperty("type", "array"); @@ -155,6 +211,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception String mode = arguments.has("mode") ? arguments.get("mode").getAsString() : "parallel"; int count = arguments.has("count") ? arguments.get("count").getAsInt() : 1; + int intervalMs = arguments.has("interval_ms") ? arguments.get("interval_ms").getAsInt() : 0; boolean allowDuplicateHeaders = arguments.has("allow_duplicate_headers") ? arguments.get("allow_duplicate_headers").getAsBoolean() : false; @@ -164,13 +221,24 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception ? arguments.getAsJsonArray("modifications") : new JsonArray(); - // フェーズ1では並列送信のみサポート - if (!"parallel".equals(mode)) { - throw new IllegalArgumentException("Phase 1 supports parallel mode only. Sequential mode will be available in Phase 2."); + JsonArray regexParams = arguments.has("regex_params") + ? arguments.getAsJsonArray("regex_params") + : new JsonArray(); + + // 送信モードの検証 + if (!"parallel".equals(mode) && !"sequential".equals(mode)) { + throw new IllegalArgumentException("mode must be 'parallel' or 'sequential'"); + } + + // 順次送信の場合、interval_msをチェック + if ("sequential".equals(mode) && intervalMs < 0) { + throw new IllegalArgumentException("interval_ms must be non-negative for sequential mode"); } log("BulkSendTool: packet_ids=" + packetIdsArray.size() + ", mode=" + mode + ", count=" + count - + ", allowDuplicateHeaders=" + allowDuplicateHeaders + ", timeout=" + timeoutMs + "ms"); + + ", interval=" + intervalMs + "ms, allowDuplicateHeaders=" + allowDuplicateHeaders + + ", timeout=" + timeoutMs + "ms, modifications=" + modifications.size() + + ", regex_params=" + regexParams.size()); // パケットIDを取得 List packetIds = new ArrayList<>(); @@ -184,15 +252,37 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception int sentCount = 0; int failedCount = 0; List results = new ArrayList<>(); + List regexParamsApplied = new ArrayList<>(); + Map extractedValues = new HashMap<>(); // regex_paramsで抽出された値を保存 try { - // 各パケットを処理 - for (int i = 0; i < packetIds.size(); i++) { - int packetId = packetIds.get(i); - BulkSendResult result = processSinglePacket(packetId, i, count, modifications, allowDuplicateHeaders); - results.add(result); - sentCount += result.sentCount; - failedCount += result.failedCount; + if ("parallel".equals(mode)) { + // 並列送信 + for (int i = 0; i < packetIds.size(); i++) { + int packetId = packetIds.get(i); + BulkSendResult result = processSinglePacket(packetId, i, count, modifications, + regexParams, extractedValues, allowDuplicateHeaders); + results.add(result); + sentCount += result.sentCount; + failedCount += result.failedCount; + regexParamsApplied.addAll(result.regexParamsApplied); + } + } else { + // 順次送信 + for (int i = 0; i < packetIds.size(); i++) { + int packetId = packetIds.get(i); + BulkSendResult result = processSinglePacketSequential(packetId, i, count, modifications, + regexParams, extractedValues, allowDuplicateHeaders, intervalMs); + results.add(result); + sentCount += result.sentCount; + failedCount += result.failedCount; + regexParamsApplied.addAll(result.regexParamsApplied); + + // 次のパケットまでインターバル(最後のパケット以外) + if (intervalMs > 0 && i < packetIds.size() - 1) { + Thread.sleep(intervalMs); + } + } } } catch (Exception e) { @@ -237,6 +327,18 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception } result.add("results", resultsArray); + // regex_params適用結果 + JsonArray regexParamsAppliedArray = new JsonArray(); + for (RegexParamApplied rpa : regexParamsApplied) { + JsonObject rpaObj = new JsonObject(); + rpaObj.addProperty("packet_index", rpa.packetIndex); + rpaObj.addProperty("pattern", rpa.pattern); + rpaObj.addProperty("extracted_value", rpa.extractedValue); + rpaObj.addProperty("applied_count", rpa.appliedCount); + regexParamsAppliedArray.add(rpaObj); + } + result.add("regex_params_applied", regexParamsAppliedArray); + // パフォーマンス統計 JsonObject performance = new JsonObject(); double packetsPerSecond = totalCount > 0 ? (double) sentCount / (executionTime / 1000.0) : 0.0; @@ -259,12 +361,14 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception * 単一パケットの処理(並列送信) */ private BulkSendResult processSinglePacket(int packetId, int packetIndex, int count, - JsonArray modifications, boolean allowDuplicateHeaders) { + JsonArray modifications, JsonArray regexParams, Map extractedValues, + boolean allowDuplicateHeaders) { BulkSendResult result = new BulkSendResult(); result.originalPacketId = packetId; result.packetIndex = packetIndex; result.newPacketIds = new ArrayList<>(); + result.regexParamsApplied = new ArrayList<>(); long startTime = System.currentTimeMillis(); @@ -287,8 +391,12 @@ private BulkSendResult processSinglePacket(int packetId, int packetIndex, int co return result; } - // 改変を適用 - OneShotPacket modifiedPacket = applyModifications(originalOneShot, modifications, packetIndex + 1, allowDuplicateHeaders); + // regex_paramsを適用 + OneShotPacket regexModifiedPacket = applyRegexParams(originalOneShot, regexParams, packetIndex, + extractedValues, result.regexParamsApplied); + + // modificationsを適用 + OneShotPacket modifiedPacket = applyModifications(regexModifiedPacket, modifications, packetIndex + 1, allowDuplicateHeaders); // 複数回送信用のパケット配列を作成 OneShotPacket[] packetsToSend = new OneShotPacket[count]; @@ -354,6 +462,85 @@ protected void done() { return result; } + /** + * 単一パケットの処理(順次送信) + */ + private BulkSendResult processSinglePacketSequential(int packetId, int packetIndex, int count, + JsonArray modifications, JsonArray regexParams, Map extractedValues, + boolean allowDuplicateHeaders, int intervalMs) { + + BulkSendResult result = new BulkSendResult(); + result.originalPacketId = packetId; + result.packetIndex = packetIndex; + result.newPacketIds = new ArrayList<>(); + result.regexParamsApplied = new ArrayList<>(); + + long startTime = System.currentTimeMillis(); + + try { + // パケットを取得 + Packet originalPacket = Packets.getInstance().query(packetId); + if (originalPacket == null) { + result.success = false; + result.failedCount = count; + result.error = "Packet with ID " + packetId + " not found"; + return result; + } + + // OneShotPacketを作成 + OneShotPacket originalOneShot = createOneShotPacket(originalPacket); + if (originalOneShot == null) { + result.success = false; + result.failedCount = count; + result.error = "Cannot create OneShotPacket from packet ID " + packetId; + return result; + } + + // 順次送信の場合、各送信で異なる処理を実行 + int successCount = 0; + int failCount = 0; + + for (int i = 0; i < count; i++) { + try { + // regex_paramsを適用(送信回数も考慮) + OneShotPacket regexModifiedPacket = applyRegexParams(originalOneShot, regexParams, packetIndex, + extractedValues, result.regexParamsApplied); + + // modificationsを適用 + OneShotPacket modifiedPacket = applyModifications(regexModifiedPacket, modifications, + packetIndex * count + i + 1, allowDuplicateHeaders); + + // 単発送信 + ResendController.getInstance().resend(modifiedPacket); + successCount++; + + // 同一パケット内の送信間隔 + if (intervalMs > 0 && i < count - 1) { + Thread.sleep(intervalMs); + } + + } catch (Exception e) { + log("BulkSendTool: Failed to send packet " + packetId + " (attempt " + (i + 1) + "): " + e.getMessage()); + failCount++; + } + } + + result.success = failCount == 0; + result.sentCount = successCount; + result.failedCount = failCount; + + } catch (Exception e) { + result.success = false; + result.failedCount = count; + result.error = e.getMessage(); + log("BulkSendTool: Failed to process packet " + packetId + ": " + e.getMessage()); + } finally { + result.executionTimeMs = System.currentTimeMillis() - startTime; + } + + return result; + } + /** * OneShotPacketを作成(ResendPacketToolと同じロジック) */ @@ -368,24 +555,312 @@ private OneShotPacket createOneShotPacket(Packet originalPacket) throws Exceptio } /** - * パケットに改変を適用(ResendPacketToolのロジックを再利用) + * regex_paramsを適用 + */ + private OneShotPacket applyRegexParams(OneShotPacket original, JsonArray regexParams, int packetIndex, + Map extractedValues, List appliedList) throws Exception { + + if (regexParams.size() == 0) { + return original; + } + + log("BulkSendTool: Applying " + regexParams.size() + " regex params to packet (index=" + packetIndex + ")"); + + byte[] data = original.getData().clone(); + String dataStr = new String(data); + + for (JsonElement paramElement : regexParams) { + JsonObject param = paramElement.getAsJsonObject(); + + // packet_indexが指定されている場合、対象パケットかチェック + if (param.has("packet_index") && param.get("packet_index").getAsInt() != packetIndex) { + continue; + } + + String pattern = param.get("pattern").getAsString(); + String valueTemplate = param.get("value_template").getAsString(); + String target = param.has("target") ? param.get("target").getAsString() : "request"; + + // 値テンプレートを処理 + String processedValue = processValueTemplate(valueTemplate, packetIndex, extractedValues); + + try { + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(dataStr); + + if (matcher.find()) { + // マッチした値を抽出(後続パケットで使用可能) + String extractedValue = matcher.group(1); + if (extractedValue != null) { + String key = "packet_" + packetIndex + "_" + pattern; + extractedValues.put(key, extractedValue); + } + + // 置換実行 + dataStr = matcher.replaceAll(processedValue); + + // 適用結果を記録 + RegexParamApplied applied = new RegexParamApplied(); + applied.packetIndex = packetIndex; + applied.pattern = pattern; + applied.extractedValue = extractedValue; + applied.appliedCount = 1; + appliedList.add(applied); + + log("BulkSendTool: Regex param applied - pattern: " + pattern + ", value: " + processedValue); + } + + } catch (Exception e) { + log("BulkSendTool: Regex param failed: " + e.getMessage()); + } + } + + data = dataStr.getBytes(); + + // 新しいOneShotPacketを作成 + OneShotPacket modifiedPacket = new OneShotPacket(original.getId(), original.getListenPort(), + original.getClient(), original.getServer(), original.getServerName(), original.getUseSSL(), data, + original.getEncoder(), original.getAlpn(), original.getDirection(), original.getConn(), + original.getGroup()); + + return modifiedPacket; + } + + /** + * value_templateを処理(ResendPacketToolのprocessReplacementVariablesを拡張) + */ + private String processValueTemplate(String template, int packetIndex, Map extractedValues) { + String result = template; + + // {{packet_index}} - パケットインデックス + result = result.replace("{{packet_index}}", String.valueOf(packetIndex)); + + // {{timestamp}} - Unix timestamp + result = result.replace("{{timestamp}}", String.valueOf(System.currentTimeMillis() / 1000)); + + // {{random}} - ランダム文字列 + if (result.contains("{{random}}")) { + String randomStr = generateRandomString(8); + result = result.replace("{{random}}", randomStr); + } + + // {{uuid}} - UUID v4 + if (result.contains("{{uuid}}")) { + String uuid = UUID.randomUUID().toString(); + result = result.replace("{{uuid}}", uuid); + } + + // {{datetime}} - ISO 8601形式日時 + if (result.contains("{{datetime}}")) { + String datetime = Instant.now().atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT); + result = result.replace("{{datetime}}", datetime); + } + + // 抽出された値を置換({{extracted:key}}形式) + for (Map.Entry entry : extractedValues.entrySet()) { + String placeholder = "{{extracted:" + entry.getKey() + "}}"; + result = result.replace(placeholder, entry.getValue()); + } + + return result; + } + + /** + * パケットに改変を適用(ResendPacketToolのロジックを完全実装) */ private OneShotPacket applyModifications(OneShotPacket original, JsonArray modifications, int index, boolean allowDuplicateHeaders) throws Exception { - // ResendPacketToolのapplyModificationsメソッドと同じ実装 - // ここでは簡略化のため、modificationsが空の場合はオリジナルを返す if (modifications.size() == 0) { return original; } log("BulkSendTool: Applying " + modifications.size() + " modifications to packet (index=" + index + ")"); - // 実際の改変処理はResendPacketToolと同じ実装を使用 - // フェーズ1では基本的な処理のみ実装 - // TODO: ResendPacketToolのapplyModificationsメソッドを共通化するか、ここで再実装 + byte[] data = original.getData().clone(); + String dataStr = new String(data); + + for (JsonElement modElement : modifications) { + JsonObject modification = modElement.getAsJsonObject(); + + String target = modification.has("target") ? modification.get("target").getAsString() : "request"; + String type = modification.get("type").getAsString(); + + log("BulkSendTool: Applying modification type=" + type + ", target=" + target); + + switch (type) { + case "regex_replace" : + dataStr = applyRegexReplace(dataStr, modification, index); + break; + case "header_add" : + dataStr = applyHeaderAdd(dataStr, modification, index, allowDuplicateHeaders); + break; + case "header_modify" : + dataStr = applyHeaderModify(dataStr, modification, index, allowDuplicateHeaders); + break; + default : + log("BulkSendTool: Unknown modification type: " + type); + break; + } + } + + data = dataStr.getBytes(); - return original; // フェーズ1では改変なしで返す + // 新しいOneShotPacketを作成 + OneShotPacket modifiedPacket = new OneShotPacket(original.getId(), original.getListenPort(), + original.getClient(), original.getServer(), original.getServerName(), original.getUseSSL(), data, + original.getEncoder(), original.getAlpn(), original.getDirection(), original.getConn(), + original.getGroup()); + + return modifiedPacket; + } + + /** + * 正規表現置換を適用(ResendPacketToolから移植) + */ + private String applyRegexReplace(String data, JsonObject modification, int index) { + String pattern = modification.get("pattern").getAsString(); + String replacement = modification.get("replacement").getAsString(); + + // 置換変数を処理 + replacement = processReplacementVariables(replacement, index); + + try { + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(data); + String result = matcher.replaceAll(replacement); + log("BulkSendTool: Regex replace applied - pattern: " + pattern + ", replacement: " + replacement); + return result; + } catch (Exception e) { + log("BulkSendTool: Regex replace failed: " + e.getMessage()); + return data; + } + } + + /** + * ヘッダー追加を適用(ResendPacketToolから移植) + */ + private String applyHeaderAdd(String data, JsonObject modification, int index, boolean allowDuplicateHeaders) { + String name = modification.get("name").getAsString(); + String value = modification.get("value").getAsString(); + + // 置換変数を処理 + value = processReplacementVariables(value, index); + + // HTTP形式のデータの場合、ヘッダー部分に追加 + if (data.contains("\r\n\r\n")) { + int headerEnd = data.indexOf("\r\n\r\n"); + String headers = data.substring(0, headerEnd); + String body = data.substring(headerEnd); + + // 重複を許可しない場合、既存ヘッダーがあるかチェック + if (!allowDuplicateHeaders) { + String pattern = "(?i)" + Pattern.quote(name) + ":\\s*[^\r\n]*"; + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(headers); + if (matcher.find()) { + // 既存ヘッダーを置換 + String result = matcher.replaceFirst(name + ": " + value) + body; + log("BulkSendTool: Header replaced (no duplicates allowed) - " + name + ": " + value); + return result; + } + } + + // 新しいヘッダーを追加 + String newHeader = name + ": " + value + "\r\n"; + String result = headers + "\r\n" + newHeader + body; + log("BulkSendTool: Header added - " + name + ": " + value); + return result; + } + + return data; + } + + /** + * ヘッダー変更を適用(ResendPacketToolから移植) + */ + private String applyHeaderModify(String data, JsonObject modification, int index, boolean allowDuplicateHeaders) { + String name = modification.get("name").getAsString(); + String value = modification.get("value").getAsString(); + + // 置換変数を処理 + value = processReplacementVariables(value, index); + + // 既存ヘッダーを置換 + String pattern = "(?i)" + Pattern.quote(name) + ":\\s*[^\r\n]*"; + + try { + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(data); + if (matcher.find()) { + String replacement = name + ": " + value; + String result; + if (allowDuplicateHeaders) { + // 重複を許可する場合は最初のヘッダーのみ変更 + result = matcher.replaceFirst(replacement); + } else { + // 重複を許可しない場合は全ての同名ヘッダーを置換 + result = matcher.replaceAll(replacement); + } + log("BulkSendTool: Header modified - " + name + ": " + value + " (allowDuplicates=" + + allowDuplicateHeaders + ")"); + return result; + } else { + // ヘッダーが見つからない場合は追加 + return applyHeaderAdd(data, modification, index, allowDuplicateHeaders); + } + } catch (Exception e) { + log("BulkSendTool: Header modify failed: " + e.getMessage()); + return data; + } + } + + /** + * 置換変数を処理(ResendPacketToolから移植) + */ + private String processReplacementVariables(String input, int index) { + String result = input; + + // {{index}} - 送信順序 + result = result.replace("{{index}}", String.valueOf(index)); + + // {{timestamp}} - Unix timestamp + result = result.replace("{{timestamp}}", String.valueOf(System.currentTimeMillis() / 1000)); + + // {{random}} - ランダム文字列 + if (result.contains("{{random}}")) { + String randomStr = generateRandomString(8); + result = result.replace("{{random}}", randomStr); + } + + // {{uuid}} - UUID v4 + if (result.contains("{{uuid}}")) { + String uuid = UUID.randomUUID().toString(); + result = result.replace("{{uuid}}", uuid); + } + + // {{datetime}} - ISO 8601形式日時 + if (result.contains("{{datetime}}")) { + String datetime = Instant.now().atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT); + result = result.replace("{{datetime}}", datetime); + } + + return result; + } + + /** + * ランダム文字列生成(ResendPacketToolから移植) + */ + private String generateRandomString(int length) { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < length; i++) { + sb.append(chars.charAt(random.nextInt(chars.length()))); + } + + return sb.toString(); } /** @@ -400,5 +875,16 @@ private static class BulkSendResult { List newPacketIds; String error; long executionTimeMs; + List regexParamsApplied; + } + + /** + * regex_paramsの適用結果 + */ + private static class RegexParamApplied { + int packetIndex; + String pattern; + String extractedValue; + int appliedCount; } } \ No newline at end of file From ac8d98a8b2c1d6d47a31517357d4d922b126afc9 Mon Sep 17 00:00:00 2001 From: kakira Date: Tue, 5 Aug 2025 01:32:12 +0900 Subject: [PATCH 21/39] =?UTF-8?q?bulk=5Fsend=20=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/mcp/tools/BulkSendTool.java | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java index 2e1cc5ba..957637bd 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java @@ -37,7 +37,13 @@ public String getName() { @Override public String getDescription() { - return "Send multiple packets in bulk with optional modifications and multiple count support"; + return "Send multiple packets in bulk with optional modifications. " + + "Use packet_ids array to specify which packets to send (can repeat same ID for multiple variations). " + + "Use regex_params to apply different modifications to each packet based on packet_index (0-based). " + + "For full header replacement, use patterns like 'User-Agent: [^\\r\\n]*'. " + + "For partial replacement, use capture groups like 'Content-Length: ([0-9]+)'. " + + "The value_template supports variables like {{timestamp}}, {{random}}, {{uuid}}, {{packet_index}}. " + + "Note: avoid using both packet_ids array with duplicates AND count parameter simultaneously to prevent unexpected multiplication of packets."; } @Override @@ -590,14 +596,36 @@ private OneShotPacket applyRegexParams(OneShotPacket original, JsonArray regexPa if (matcher.find()) { // マッチした値を抽出(後続パケットで使用可能) - String extractedValue = matcher.group(1); - if (extractedValue != null) { - String key = "packet_" + packetIndex + "_" + pattern; - extractedValues.put(key, extractedValue); + String extractedValue = null; + try { + // キャプチャグループがあるかチェック + if (matcher.groupCount() > 0) { + extractedValue = matcher.group(1); + } else { + // キャプチャグループがない場合は全体をマッチ + extractedValue = matcher.group(0); + } + + if (extractedValue != null) { + String key = "packet_" + packetIndex + "_" + pattern; + extractedValues.put(key, extractedValue); + } + } catch (Exception ex) { + log("BulkSendTool: Failed to extract value: " + ex.getMessage()); } // 置換実行 + String beforeReplace = dataStr; dataStr = matcher.replaceAll(processedValue); + + // デバッグログ: 置換前後の比較 + if (!beforeReplace.equals(dataStr)) { + log("BulkSendTool: Replacement successful - pattern: " + pattern); + log("BulkSendTool: Before: " + beforeReplace.substring(Math.max(0, matcher.start() - 20), Math.min(beforeReplace.length(), matcher.end() + 20))); + log("BulkSendTool: After: " + dataStr.substring(Math.max(0, dataStr.indexOf(processedValue) - 20), Math.min(dataStr.length(), dataStr.indexOf(processedValue) + processedValue.length() + 20))); + } else { + log("BulkSendTool: Warning: No replacement occurred for pattern: " + pattern); + } // 適用結果を記録 RegexParamApplied applied = new RegexParamApplied(); @@ -608,10 +636,16 @@ private OneShotPacket applyRegexParams(OneShotPacket original, JsonArray regexPa appliedList.add(applied); log("BulkSendTool: Regex param applied - pattern: " + pattern + ", value: " + processedValue); + } else { + log("BulkSendTool: Pattern not found in data - pattern: " + pattern); + // デバッグ用: データの一部を表示 + String debugData = dataStr.length() > 200 ? dataStr.substring(0, 200) + "..." : dataStr; + log("BulkSendTool: Data sample: " + debugData.replace("\r\n", "\\r\\n")); } } catch (Exception e) { log("BulkSendTool: Regex param failed: " + e.getMessage()); + e.printStackTrace(); } } From 1851764ddb853ef78cfa864668cd9a3faa453b07 Mon Sep 17 00:00:00 2001 From: kakira Date: Tue, 5 Aug 2025 12:34:16 +0900 Subject: [PATCH 22/39] spotlessApply --- .../extensions/mcp/tools/BulkSendTool.java | 101 +++++++++--------- 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java index 957637bd..223eaae1 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java @@ -25,8 +25,7 @@ import packetproxy.model.Packets; /** - * 複数パケット一括送信ツール - * フェーズ2: 順次送信モード、modifications適用、regex_params機能 + * 複数パケット一括送信ツール フェーズ2: 順次送信モード、modifications適用、regex_params機能 */ public class BulkSendTool extends AuthenticatedMCPTool { @@ -37,13 +36,13 @@ public String getName() { @Override public String getDescription() { - return "Send multiple packets in bulk with optional modifications. " + - "Use packet_ids array to specify which packets to send (can repeat same ID for multiple variations). " + - "Use regex_params to apply different modifications to each packet based on packet_index (0-based). " + - "For full header replacement, use patterns like 'User-Agent: [^\\r\\n]*'. " + - "For partial replacement, use capture groups like 'Content-Length: ([0-9]+)'. " + - "The value_template supports variables like {{timestamp}}, {{random}}, {{uuid}}, {{packet_index}}. " + - "Note: avoid using both packet_ids array with duplicates AND count parameter simultaneously to prevent unexpected multiplication of packets."; + return "Send multiple packets in bulk with optional modifications. " + + "Use packet_ids array to specify which packets to send (can repeat same ID for multiple variations). " + + "Use regex_params to apply different modifications to each packet based on packet_index (0-based). " + + "For full header replacement, use patterns like 'User-Agent: [^\\r\\n]*'. " + + "For partial replacement, use capture groups like 'Content-Length: ([0-9]+)'. " + + "The value_template supports variables like {{timestamp}}, {{random}}, {{uuid}}, {{packet_index}}. " + + "Note: avoid using both packet_ids array with duplicates AND count parameter simultaneously to prevent unexpected multiplication of packets."; } @Override @@ -82,7 +81,8 @@ public JsonObject getInputSchema() { // interval_ms (optional) JsonObject intervalProp = new JsonObject(); intervalProp.addProperty("type", "integer"); - intervalProp.addProperty("description", "Interval between sends in milliseconds (sequential mode only, default: 0, maximum: 60000)"); + intervalProp.addProperty("description", + "Interval between sends in milliseconds (sequential mode only, default: 0, maximum: 60000)"); intervalProp.addProperty("default", 0); intervalProp.addProperty("minimum", 0); intervalProp.addProperty("maximum", 60000); @@ -108,7 +108,8 @@ public JsonObject getInputSchema() { JsonObject valueTemplateProp = new JsonObject(); valueTemplateProp.addProperty("type", "string"); - valueTemplateProp.addProperty("description", "Template with variables: {{packet_index}}, {{timestamp}}, {{random}}, {{uuid}}"); + valueTemplateProp.addProperty("description", + "Template with variables: {{packet_index}}, {{timestamp}}, {{random}}, {{uuid}}"); regexParamProps.add("value_template", valueTemplateProp); JsonObject regexTargetProp = new JsonObject(); @@ -241,10 +242,9 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception throw new IllegalArgumentException("interval_ms must be non-negative for sequential mode"); } - log("BulkSendTool: packet_ids=" + packetIdsArray.size() + ", mode=" + mode + ", count=" + count - + ", interval=" + intervalMs + "ms, allowDuplicateHeaders=" + allowDuplicateHeaders - + ", timeout=" + timeoutMs + "ms, modifications=" + modifications.size() - + ", regex_params=" + regexParams.size()); + log("BulkSendTool: packet_ids=" + packetIdsArray.size() + ", mode=" + mode + ", count=" + count + ", interval=" + + intervalMs + "ms, allowDuplicateHeaders=" + allowDuplicateHeaders + ", timeout=" + timeoutMs + + "ms, modifications=" + modifications.size() + ", regex_params=" + regexParams.size()); // パケットIDを取得 List packetIds = new ArrayList<>(); @@ -266,8 +266,8 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception // 並列送信 for (int i = 0; i < packetIds.size(); i++) { int packetId = packetIds.get(i); - BulkSendResult result = processSinglePacket(packetId, i, count, modifications, - regexParams, extractedValues, allowDuplicateHeaders); + BulkSendResult result = processSinglePacket(packetId, i, count, modifications, regexParams, + extractedValues, allowDuplicateHeaders); results.add(result); sentCount += result.sentCount; failedCount += result.failedCount; @@ -283,7 +283,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception sentCount += result.sentCount; failedCount += result.failedCount; regexParamsApplied.addAll(result.regexParamsApplied); - + // 次のパケットまでインターバル(最後のパケット以外) if (intervalMs > 0 && i < packetIds.size() - 1) { Thread.sleep(intervalMs); @@ -317,18 +317,18 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception resultObj.addProperty("success", r.success); resultObj.addProperty("sent_count", r.sentCount); resultObj.addProperty("failed_count", r.failedCount); - + JsonArray newPacketIds = new JsonArray(); for (Integer id : r.newPacketIds) { newPacketIds.add(id); } resultObj.add("new_packet_ids", newPacketIds); - + if (r.error != null) { resultObj.addProperty("error", r.error); } resultObj.addProperty("execution_time_ms", r.executionTimeMs); - + resultsArray.add(resultObj); } result.add("results", resultsArray); @@ -348,10 +348,10 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception // パフォーマンス統計 JsonObject performance = new JsonObject(); double packetsPerSecond = totalCount > 0 ? (double) sentCount / (executionTime / 1000.0) : 0.0; - double avgResponseTime = results.size() > 0 - ? results.stream().mapToLong(r -> r.executionTimeMs).average().orElse(0.0) - : 0.0; - + double avgResponseTime = results.size() > 0 + ? results.stream().mapToLong(r -> r.executionTimeMs).average().orElse(0.0) + : 0.0; + performance.addProperty("packets_per_second", Math.round(packetsPerSecond * 100.0) / 100.0); performance.addProperty("average_response_time_ms", Math.round(avgResponseTime)); performance.addProperty("concurrent_connections", totalPackets); @@ -359,25 +359,25 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception result.add("job_id", null); // 非同期実行はフェーズ3で実装 - log("BulkSendTool: Completed. Sent: " + sentCount + ", Failed: " + failedCount + ", Time: " + executionTime + "ms"); + log("BulkSendTool: Completed. Sent: " + sentCount + ", Failed: " + failedCount + ", Time: " + executionTime + + "ms"); return result; } /** * 単一パケットの処理(並列送信) */ - private BulkSendResult processSinglePacket(int packetId, int packetIndex, int count, - JsonArray modifications, JsonArray regexParams, Map extractedValues, - boolean allowDuplicateHeaders) { - + private BulkSendResult processSinglePacket(int packetId, int packetIndex, int count, JsonArray modifications, + JsonArray regexParams, Map extractedValues, boolean allowDuplicateHeaders) { + BulkSendResult result = new BulkSendResult(); result.originalPacketId = packetId; result.packetIndex = packetIndex; result.newPacketIds = new ArrayList<>(); result.regexParamsApplied = new ArrayList<>(); - + long startTime = System.currentTimeMillis(); - + try { // パケットを取得 Packet originalPacket = Packets.getInstance().query(packetId); @@ -398,11 +398,12 @@ private BulkSendResult processSinglePacket(int packetId, int packetIndex, int co } // regex_paramsを適用 - OneShotPacket regexModifiedPacket = applyRegexParams(originalOneShot, regexParams, packetIndex, + OneShotPacket regexModifiedPacket = applyRegexParams(originalOneShot, regexParams, packetIndex, extractedValues, result.regexParamsApplied); // modificationsを適用 - OneShotPacket modifiedPacket = applyModifications(regexModifiedPacket, modifications, packetIndex + 1, allowDuplicateHeaders); + OneShotPacket modifiedPacket = applyModifications(regexModifiedPacket, modifications, packetIndex + 1, + allowDuplicateHeaders); // 複数回送信用のパケット配列を作成 OneShotPacket[] packetsToSend = new OneShotPacket[count]; @@ -471,18 +472,18 @@ protected void done() { /** * 単一パケットの処理(順次送信) */ - private BulkSendResult processSinglePacketSequential(int packetId, int packetIndex, int count, + private BulkSendResult processSinglePacketSequential(int packetId, int packetIndex, int count, JsonArray modifications, JsonArray regexParams, Map extractedValues, boolean allowDuplicateHeaders, int intervalMs) { - + BulkSendResult result = new BulkSendResult(); result.originalPacketId = packetId; result.packetIndex = packetIndex; result.newPacketIds = new ArrayList<>(); result.regexParamsApplied = new ArrayList<>(); - + long startTime = System.currentTimeMillis(); - + try { // パケットを取得 Packet originalPacket = Packets.getInstance().query(packetId); @@ -509,11 +510,11 @@ private BulkSendResult processSinglePacketSequential(int packetId, int packetInd for (int i = 0; i < count; i++) { try { // regex_paramsを適用(送信回数も考慮) - OneShotPacket regexModifiedPacket = applyRegexParams(originalOneShot, regexParams, packetIndex, + OneShotPacket regexModifiedPacket = applyRegexParams(originalOneShot, regexParams, packetIndex, extractedValues, result.regexParamsApplied); // modificationsを適用 - OneShotPacket modifiedPacket = applyModifications(regexModifiedPacket, modifications, + OneShotPacket modifiedPacket = applyModifications(regexModifiedPacket, modifications, packetIndex * count + i + 1, allowDuplicateHeaders); // 単発送信 @@ -526,7 +527,8 @@ private BulkSendResult processSinglePacketSequential(int packetId, int packetInd } } catch (Exception e) { - log("BulkSendTool: Failed to send packet " + packetId + " (attempt " + (i + 1) + "): " + e.getMessage()); + log("BulkSendTool: Failed to send packet " + packetId + " (attempt " + (i + 1) + "): " + + e.getMessage()); failCount++; } } @@ -565,7 +567,7 @@ private OneShotPacket createOneShotPacket(Packet originalPacket) throws Exceptio */ private OneShotPacket applyRegexParams(OneShotPacket original, JsonArray regexParams, int packetIndex, Map extractedValues, List appliedList) throws Exception { - + if (regexParams.size() == 0) { return original; } @@ -605,7 +607,7 @@ private OneShotPacket applyRegexParams(OneShotPacket original, JsonArray regexPa // キャプチャグループがない場合は全体をマッチ extractedValue = matcher.group(0); } - + if (extractedValue != null) { String key = "packet_" + packetIndex + "_" + pattern; extractedValues.put(key, extractedValue); @@ -617,12 +619,15 @@ private OneShotPacket applyRegexParams(OneShotPacket original, JsonArray regexPa // 置換実行 String beforeReplace = dataStr; dataStr = matcher.replaceAll(processedValue); - + // デバッグログ: 置換前後の比較 if (!beforeReplace.equals(dataStr)) { log("BulkSendTool: Replacement successful - pattern: " + pattern); - log("BulkSendTool: Before: " + beforeReplace.substring(Math.max(0, matcher.start() - 20), Math.min(beforeReplace.length(), matcher.end() + 20))); - log("BulkSendTool: After: " + dataStr.substring(Math.max(0, dataStr.indexOf(processedValue) - 20), Math.min(dataStr.length(), dataStr.indexOf(processedValue) + processedValue.length() + 20))); + log("BulkSendTool: Before: " + beforeReplace.substring(Math.max(0, matcher.start() - 20), + Math.min(beforeReplace.length(), matcher.end() + 20))); + log("BulkSendTool: After: " + dataStr + .substring(Math.max(0, dataStr.indexOf(processedValue) - 20), Math.min(dataStr.length(), + dataStr.indexOf(processedValue) + processedValue.length() + 20))); } else { log("BulkSendTool: Warning: No replacement occurred for pattern: " + pattern); } @@ -704,7 +709,7 @@ private String processValueTemplate(String template, int packetIndex, Map Date: Sun, 10 Aug 2025 22:08:59 +0900 Subject: [PATCH 23/39] =?UTF-8?q?=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E8=A1=A8=E7=A4=BA=E6=8A=91=E5=88=B6=E3=82=AA=E3=83=97?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-spec.md | 25 ++++++++++++++++++- .../packetproxy/common/ConfigHttpServer.java | 24 +++++++++++------- .../mcp/tools/RestoreConfigTool.java | 10 +++++++- .../mcp/tools/UpdateConfigTool.java | 20 +++++++++++---- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index fab2c6fb..8577a604 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -336,6 +336,7 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 ] }, "backup": true, + "suppress_dialog": false, "access_token": "your_access_token_here" } }, @@ -347,12 +348,19 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 - `access_token` (string, required): PacketProxy設定のアクセストークン - `config_json` (object, required): PacketProxyHub互換の設定JSON(完全な形式) - `backup` (boolean, optional): 既存設定をバックアップ (デフォルト: true) +- `suppress_dialog` (boolean, optional): 確認ダイアログを非表示にする (デフォルト: false) **設定削除について:** - `config_json`に含まれないIDの項目は自動的に削除されます - 例: serversに`id:1`のみ含まれている場合、`id:2,3...`のサーバーは削除されます - HTTP APIは既存設定を完全に置き換える方式で動作します +**ダイアログ制御について:** +- `suppress_dialog: false` (デフォルト): 設定上書き前に確認ダイアログを表示 +- `suppress_dialog: true`: 確認ダイアログを表示せずに自動的に設定を上書き +- ダイアログが表示される場合、ユーザーが「はい」を選択した場合のみ設定が適用されます +- ダイアログで「いいえ」を選択した場合、HTTP 401エラーが返されます + **レスポンス:** ```json @@ -384,7 +392,8 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 "name": "restore_config", "arguments": { "access_token": "your_access_token_here", - "backup_id": "backup_20250115_103000" + "backup_id": "backup_20250115_103000", + "suppress_dialog": false } }, "id": 6 @@ -394,6 +403,13 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 **パラメータ:** - `access_token` (string, required): PacketProxy設定のアクセストークン - `backup_id` (string, required): 復元するバックアップID +- `suppress_dialog` (boolean, optional): 確認ダイアログを非表示にする (デフォルト: false) + +**ダイアログ制御について:** +- `suppress_dialog: false` (デフォルト): 設定復元前に確認ダイアログを表示 +- `suppress_dialog: true`: 確認ダイアログを表示せずに自動的に設定を復元 +- ダイアログが表示される場合、ユーザーが「はい」を選択した場合のみ設定が適用されます +- ダイアログで「いいえ」を選択した場合、HTTP 401エラーが返されます **レスポンス:** @@ -710,6 +726,13 @@ GET /mcp/logs?level=info # ログ取得 POST /mcp/restore/{backup_id} # バックアップ復元 ``` +### HTTP ヘッダー制御 + +設定更新API (`POST /config`) では以下の特別なHTTPヘッダーをサポートします: + +- `X-Suppress-Dialog: true`: 確認ダイアログを非表示にして自動的に設定を上書き +- `X-Suppress-Dialog: false` (デフォルト): 確認ダイアログを表示 + ### 認証 HTTP APIはアクセストークンによる認証を使用します。 diff --git a/src/main/java/core/packetproxy/common/ConfigHttpServer.java b/src/main/java/core/packetproxy/common/ConfigHttpServer.java index 779645ee..4f531770 100644 --- a/src/main/java/core/packetproxy/common/ConfigHttpServer.java +++ b/src/main/java/core/packetproxy/common/ConfigHttpServer.java @@ -130,20 +130,26 @@ public Response serve(IHTTPSession session) { try { - GUIMain.getInstance().setAlwaysOnTop(true); - GUIMain.getInstance().setVisible(true); + // Check if dialog suppression is requested + String suppressDialog = session.getHeaders().get("x-suppress-dialog"); + boolean showDialog = !"true".equals(suppressDialog); - GUIMain.getInstance().getTabbedPane().setSelectedIndex(GUIMain.Panes.OPTIONS.ordinal()); + if (showDialog) { + GUIMain.getInstance().setAlwaysOnTop(true); + GUIMain.getInstance().setVisible(true); - int option = JOptionPane.showConfirmDialog(GUIMain.getInstance(), - I18nString.get("Do you want to overwrite config?"), I18nString.get("Loading config"), - JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + GUIMain.getInstance().getTabbedPane().setSelectedIndex(GUIMain.Panes.OPTIONS.ordinal()); - GUIMain.getInstance().setAlwaysOnTop(false); + int option = JOptionPane.showConfirmDialog(GUIMain.getInstance(), + I18nString.get("Do you want to overwrite config?"), I18nString.get("Loading config"), + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); - if (option == JOptionPane.NO_OPTION) { + GUIMain.getInstance().setAlwaysOnTop(false); - return NanoHTTPD.newFixedLengthResponse(Response.Status.UNAUTHORIZED, MIME_HTML, null); + if (option == JOptionPane.NO_OPTION) { + + return NanoHTTPD.newFixedLengthResponse(Response.Status.UNAUTHORIZED, MIME_HTML, null); + } } HashMap map = new HashMap(); diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java index c9fc9ecd..341ab728 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java @@ -20,7 +20,7 @@ public String getName() { @Override public String getDescription() { - return "Restore PacketProxy configuration from backup file"; + return "Restore PacketProxy configuration from backup file with optional dialog suppression"; } @Override @@ -32,6 +32,12 @@ public JsonObject getInputSchema() { backupIdProp.addProperty("description", "Backup ID to restore from (e.g., backup_20250103_120000)"); schema.add("backup_id", backupIdProp); + JsonObject suppressDialogProp = new JsonObject(); + suppressDialogProp.addProperty("type", "boolean"); + suppressDialogProp.addProperty("description", "Suppress confirmation dialog for configuration restore (default: false)"); + suppressDialogProp.addProperty("default", false); + schema.add("suppress_dialog", suppressDialogProp); + return addAccessTokenToSchema(schema); } @@ -44,6 +50,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception } String backupId = arguments.get("backup_id").getAsString(); + boolean suppressDialog = arguments.has("suppress_dialog") ? arguments.get("suppress_dialog").getAsBoolean() : false; try { log("RestoreConfigTool step 1: Loading backup configuration"); @@ -54,6 +61,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception JsonObject updateArgs = new JsonObject(); updateArgs.add("config_json", backupConfig); updateArgs.addProperty("backup", true); + updateArgs.addProperty("suppress_dialog", suppressDialog); updateArgs.addProperty("access_token", arguments.get("access_token").getAsString()); UpdateConfigTool updateTool = new UpdateConfigTool(); diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java index 10b16b70..56fb1aaa 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java @@ -28,7 +28,7 @@ public String getName() { @Override public String getDescription() { - return "Update PacketProxy configuration settings"; + return "Update PacketProxy configuration settings with optional backup and dialog suppression"; } @Override @@ -46,6 +46,12 @@ public JsonObject getInputSchema() { backupProp.addProperty("default", true); schema.add("backup", backupProp); + JsonObject suppressDialogProp = new JsonObject(); + suppressDialogProp.addProperty("type", "boolean"); + suppressDialogProp.addProperty("description", "Suppress confirmation dialog for configuration update (default: false)"); + suppressDialogProp.addProperty("default", false); + schema.add("suppress_dialog", suppressDialogProp); + return addAccessTokenToSchema(schema); } @@ -59,6 +65,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception JsonObject configJson = arguments.getAsJsonObject("config_json"); boolean backup = arguments.has("backup") ? arguments.get("backup").getAsBoolean() : true; + boolean suppressDialog = arguments.has("suppress_dialog") ? arguments.get("suppress_dialog").getAsBoolean() : false; try { log("UpdateConfigTool step 1: Starting configuration update"); @@ -72,7 +79,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception } log("UpdateConfigTool step 4: Updating configuration"); - updateConfiguration(configJson); + updateConfiguration(configJson, suppressDialog); log("UpdateConfigTool step 5: Configuration updated successfully"); log("UpdateConfigTool step 6: Building response data"); @@ -154,16 +161,16 @@ private JsonObject createConfigBackup() throws Exception { return backupInfo; } - private void updateConfiguration(JsonObject configJson) throws Exception { + private void updateConfiguration(JsonObject configJson, boolean suppressDialog) throws Exception { log("UpdateConfigTool starting configuration update using HTTP API"); // HTTP POST APIで設定を更新(削除処理も自動実行) - updateConfigViaHttpApi(configJson.toString()); + updateConfigViaHttpApi(configJson.toString(), suppressDialog); log("UpdateConfigTool configuration update completed using HTTP API"); } - private void updateConfigViaHttpApi(String configJsonString) throws Exception { + private void updateConfigViaHttpApi(String configJsonString, boolean suppressDialog) throws Exception { // 設定済みAccessTokenを取得(HTTPリクエスト用) String accessToken = getConfiguredAccessToken(); @@ -173,6 +180,9 @@ private void updateConfigViaHttpApi(String configJsonString) throws Exception { conn.setRequestMethod("POST"); conn.setRequestProperty("Authorization", accessToken); conn.setRequestProperty("Content-Type", "application/json"); + if (suppressDialog) { + conn.setRequestProperty("X-Suppress-Dialog", "true"); + } conn.setDoOutput(true); conn.setConnectTimeout(5000); conn.setReadTimeout(10000); From 237efc234deadf20490e62847ed3705daca1c81a Mon Sep 17 00:00:00 2001 From: kakira Date: Sun, 10 Aug 2025 22:26:12 +0900 Subject: [PATCH 24/39] =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E6=99=82=E3=81=AE500=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92?= =?UTF-8?q?=E6=8A=91=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-spec.md | 21 +++++++++++++++++-- .../mcp/tools/UpdateConfigTool.java | 4 ++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index 8577a604..06479f73 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -279,7 +279,8 @@ PacketProxyの設定情報をHTTP API (`http://localhost:32349/config`) 経由 ### 5. `update_config` - 設定変更 -PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変更します。PacketProxyHub互換の形式を使用し、指定されたIDが含まれない項目は自動的に削除されます。 +Update PacketProxy configuration settings with complete configuration object. +IMPORTANT: Requires a complete configuration object, not partial updates. **リクエスト:** @@ -346,10 +347,26 @@ PacketProxyの設定をHTTP API (`http://localhost:32349/config`) 経由で変 **パラメータ:** - `access_token` (string, required): PacketProxy設定のアクセストークン -- `config_json` (object, required): PacketProxyHub互換の設定JSON(完全な形式) +- `config_json` (object, required): PacketProxyHub-compatible configuration JSON containing COMPLETE configuration object. Must include all required arrays: listenPorts, servers, modifications, sslPassThroughs (can be empty arrays). Partial configurations will cause null pointer errors. Recommended workflow: 1) Call get_config() first, 2) Modify specific fields in the returned object, 3) Pass the entire modified object here. - `backup` (boolean, optional): 既存設定をバックアップ (デフォルト: true) - `suppress_dialog` (boolean, optional): 確認ダイアログを非表示にする (デフォルト: false) +**重要な注意事項:** + +**完全な設定オブジェクトが必要:** +- `config_json`は部分的な設定ではなく、**完全な設定オブジェクト**である必要があります +- 以下の配列は必須です(空配列でも可): + - `listenPorts`: リッスンポート設定 + - `servers`: サーバー設定 + - `modifications`: 改変設定 + - `sslPassThroughs`: SSL パススルー設定 +- 部分的な設定を渡すとnull pointerエラーが発生します + +**推奨ワークフロー:** +1. 最初に`get_config()`を呼び出して現在の完全な設定を取得 +2. 取得した設定オブジェクトの特定のフィールドを変更 +3. 変更した完全なオブジェクトを`update_config()`に渡す + **設定削除について:** - `config_json`に含まれないIDの項目は自動的に削除されます - 例: serversに`id:1`のみ含まれている場合、`id:2,3...`のサーバーは削除されます diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java index 56fb1aaa..6d5fae87 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java @@ -28,7 +28,7 @@ public String getName() { @Override public String getDescription() { - return "Update PacketProxy configuration settings with optional backup and dialog suppression"; + return "Update PacketProxy configuration settings with complete configuration object. IMPORTANT: Requires a complete configuration object, not partial updates."; } @Override @@ -37,7 +37,7 @@ public JsonObject getInputSchema() { JsonObject configJsonProp = new JsonObject(); configJsonProp.addProperty("type", "object"); - configJsonProp.addProperty("description", "PacketProxyHub-compatible configuration JSON"); + configJsonProp.addProperty("description", "PacketProxyHub-compatible configuration JSON containing COMPLETE configuration object. Must include all required arrays: listenPorts, servers, modifications, sslPassThroughs (can be empty arrays). Partial configurations will cause null pointer errors. Recommended workflow: 1) Call get_config() first, 2) Modify specific fields in the returned object, 3) Pass the entire modified object here."); schema.add("config_json", configJsonProp); JsonObject backupProp = new JsonObject(); From 89547dafe6b6f3a7f5860144ae67344d1046ad1e Mon Sep 17 00:00:00 2001 From: kakira Date: Sun, 10 Aug 2025 22:30:36 +0900 Subject: [PATCH 25/39] =?UTF-8?q?Timeout=20=E3=82=92=201=E5=88=86=E3=81=AB?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/mcp-http-bridge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mcp-http-bridge.js b/scripts/mcp-http-bridge.js index 6ec7ff53..327699b5 100755 --- a/scripts/mcp-http-bridge.js +++ b/scripts/mcp-http-bridge.js @@ -232,7 +232,7 @@ class MCPHttpBridge { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }, - timeout: 5000 // 5 second timeout + timeout: 60000 // 60 second timeout }; const req = http.request(PACKETPROXY_HTTP_URL, options, (res) => { From 04e0d068d25375295ea7da0dc1db1c7b4a4cf426 Mon Sep 17 00:00:00 2001 From: kakira Date: Sun, 10 Aug 2025 22:37:10 +0900 Subject: [PATCH 26/39] =?UTF-8?q?access=5Ftoken=20=E3=81=AE=E8=AA=AC?= =?UTF-8?q?=E6=98=8E=E3=82=92=E5=A4=89=E6=9B=B4=E3=81=97=E3=81=A6Tool=20ca?= =?UTF-8?q?ll=E3=82=92=E5=AE=89=E5=AE=9A=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/mcp/tools/AuthenticatedMCPTool.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java index 1106927d..4120bacb 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java @@ -21,9 +21,15 @@ protected void validateAccessToken(JsonObject arguments) throws Exception { } String providedToken = arguments.get("access_token").getAsString(); - if (providedToken == null || providedToken.trim().isEmpty()) { + if (providedToken == null) { throw new Exception( - "access_token cannot be empty. Please provide your PacketProxy access token from Settings > Import/Export configs section."); + "access_token parameter is required. Please provide your PacketProxy access token from Settings or leave empty (\"\") to use environment variable."); + } + + // 空文字列の場合は環境変数から取得する想定なのでvalidationをスキップ + if (providedToken.trim().isEmpty()) { + log("Empty access_token provided, assuming environment variable usage"); + return; } // PacketProxy設定からAccessTokenを取得 @@ -60,7 +66,7 @@ protected String getConfiguredAccessToken() throws Exception { protected JsonObject addAccessTokenToSchema(JsonObject schema) { JsonObject accessTokenProp = new JsonObject(); accessTokenProp.addProperty("type", "string"); - accessTokenProp.addProperty("description", "Access token for authentication"); + accessTokenProp.addProperty("description", "Access token for authentication. Leave empty (\"\") to use environment variable (handled by scripts/mcp-http-bridge.js), or provide explicit token string"); schema.add("access_token", accessTokenProp); return schema; } From d77a2c44a08aa79da4065691752ebb9c3ad990cc Mon Sep 17 00:00:00 2001 From: kakira Date: Sun, 10 Aug 2025 22:42:57 +0900 Subject: [PATCH 27/39] =?UTF-8?q?Timeout=20=E3=82=92=201=E5=88=86=E3=81=AB?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../packetproxy/extensions/mcp/tools/UpdateConfigTool.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java index 6d5fae87..f87e1296 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java @@ -185,7 +185,7 @@ private void updateConfigViaHttpApi(String configJsonString, boolean suppressDia } conn.setDoOutput(true); conn.setConnectTimeout(5000); - conn.setReadTimeout(10000); + conn.setReadTimeout(60000); // リクエストボディを送信 try (OutputStream os = conn.getOutputStream()) { @@ -236,7 +236,7 @@ private String getConfigFromHttpApiForBackup() throws Exception { conn.setRequestMethod("GET"); conn.setRequestProperty("Authorization", accessToken); conn.setConnectTimeout(5000); - conn.setReadTimeout(10000); + conn.setReadTimeout(60000); int responseCode = conn.getResponseCode(); if (responseCode != 200) { From 45a706ff5c31998746cb3762618d5d57c56902c8 Mon Sep 17 00:00:00 2001 From: kakira Date: Sun, 10 Aug 2025 23:19:21 +0900 Subject: [PATCH 28/39] =?UTF-8?q?call=5Fvulcheck=5Fhelper=20=E3=83=84?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-spec.md | 202 +++++++ .../extensions/mcp/tools/ToolRegistry.java | 1 + .../mcp/tools/VulCheckHelperTool.java | 553 ++++++++++++++++++ 3 files changed, 756 insertions(+) create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index 06479f73..34d078c1 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -658,6 +658,207 @@ IMPORTANT: Requires a complete configuration object, not partial updates. } ``` +### 9. `call_vulcheck_helper` - VulCheck脆弱性テストヘルパー + +指定されたパケットにVulCheckテストケースを適用して、自動的にペイロードを生成し、指定された位置に注入してバッチ送信を実行します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "call_vulcheck_helper", + "arguments": { + "access_token": "your_access_token_here", + "packet_id": 123, + "vulcheck_type": "Number", + "target_locations": [ + { + "pattern": "userId=\\d+", + "replacement": "userId=$1", + "description": "User ID parameter" + }, + { + "pattern": "amount=[0-9.]+", + "replacement": "amount=$1", + "description": "Amount field" + } + ], + "interval_ms": 100, + "mode": "sequential", + "max_payloads": 50, + "timeout_ms": 300000 + } + }, + "id": 9 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `packet_id` (number, required): VulCheckテストのベースとして使用するパケットID +- `vulcheck_type` (string, required): 実行するVulCheckの種類 (Number, JWT等)。'list'を指定すると利用可能なタイプ一覧を取得 +- `target_locations` (array, required): VulCheckペイロードを注入するパケット内の対象位置の配列。正規表現パターンまたは位置範囲で指定可能 + - **正規表現アプローチ (推奨):** + - `pattern` (string, required): マッチ対象の正規表現パターン (例: `"sessionId=\\w+"`) + - `replacement` (string, optional): 置換テンプレート。省略時はマッチ全体を置換。`$1`でペイロードを表す (例: `"sessionId=$1"`) + - `description` (string, optional): この対象位置の説明 + - **位置範囲アプローチ (後方互換性のため保持):** + - `start` (number, required): 対象位置の開始位置 + - `end` (number, required): 対象位置の終了位置 + - `description` (string, optional): この対象位置の説明 +- `interval_ms` (number, optional): パケット送信間隔(ms) (デフォルト: 100) +- `mode` (string, optional): 実行モード "sequential" | "parallel" (デフォルト: "sequential") +- `max_payloads` (number, optional): 位置ごとの最大ペイロード生成数 (デフォルト: 50, 最大: 1000) +- `timeout_ms` (number, optional): 全体操作タイムアウト(ms) (デフォルト: 300000 - 5分) + +**実行モード:** +- `sequential`: ペイロードを順次送信 (間隔制御あり) +- `parallel`: 全ペイロードを並列送信 (高速、interval_msは無視) + +**利用可能なVulCheckタイプ取得:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "call_vulcheck_helper", + "arguments": { + "access_token": "your_access_token_here", + "packet_id": 123, + "vulcheck_type": "list" + } + }, + "id": 10 +} +``` + +**VulCheckタイプ一覧レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "available_vulcheck_types": "Number, JWT", + "vulcheck_types": [ + { + "name": "Number", + "description": "VulCheck tests for Number vulnerabilities", + "generators": [ + {"name": "NegativeNumber", "generate_on_start": true}, + {"name": "Zero", "generate_on_start": true}, + {"name": "Decimals", "generate_on_start": true}, + {"name": "IntegerOverflow", "generate_on_start": true} + ], + "generator_count": 9 + }, + { + "name": "JWT", + "description": "VulCheck tests for JWT vulnerabilities", + "generators": [ + {"name": "JWTPayloadModified", "generate_on_start": false}, + {"name": "JWTHeaderAlgNone", "generate_on_start": false} + ], + "generator_count": 8 + } + ], + "total_types": 2 + }, + "id": 10 +} +``` + +**実行結果レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "success": true, + "vulcheck_type": "Number", + "mode": "sequential", + "total_locations": 2, + "total_payloads_generated": 18, + "total_packets_sent": 16, + "total_failed": 2, + "execution_time_ms": 2400, + "location_results": [ + { + "start": 45, + "end": 50, + "description": "User ID parameter (match 1)", + "payloads_generated": 9, + "packets_sent": 8, + "packets_failed": 1, + "execution_time_ms": 1200, + "generated_payloads": [ + "-1", "0", "0.1", "2147483647", "2147483648", + "-2147483649", "9223372036854775807", "9223372036854775808", + "-9223372036854775809" + ] + }, + { + "start": 78, + "end": 85, + "description": "Amount field (match 1)", + "payloads_generated": 9, + "packets_sent": 8, + "packets_failed": 1, + "execution_time_ms": 1200, + "generated_payloads": [ + "-1", "0", "0.1", "2147483647", "2147483648", + "-2147483649", "9223372036854775807", "9223372036854775808", + "-9223372036854775809" + ] + } + ], + "performance": { + "average_interval_ms": 100, + "payloads_per_second": 6.67, + "success_rate_percent": 88.89 + } + }, + "id": 9 +} +``` + +**使用例:** + +```bash +# 利用可能なVulCheckタイプを確認 +curl -X POST http://localhost:32349/mcp/tools/call \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your_access_token" \ + -d '{ + "name": "call_vulcheck_helper", + "arguments": { + "packet_id": 123, + "vulcheck_type": "list" + } + }' + +# Number VulCheckを実行 +curl -X POST http://localhost:32349/mcp/tools/call \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your_access_token" \ + -d '{ + "name": "call_vulcheck_helper", + "arguments": { + "packet_id": 123, + "vulcheck_type": "Number", + "target_locations": [ + {"pattern": "userId=\\d+", "replacement": "userId=$1", "description": "User ID"} + ], + "mode": "sequential", + "interval_ms": 200, + "max_payloads": 10 + } + }' +``` + ## フィルタ構文仕様 PacketProxyのFilterTextParserに準拠した構文を使用します。 @@ -739,6 +940,7 @@ GET /mcp/configs # 設定一覧 PUT /mcp/configs # 設定更新 POST /mcp/resend/{packet_id} # パケット再送 POST /mcp/bulk_send # 複数パケット一括送信 +POST /mcp/call_vulcheck_helper # VulCheck脆弱性テストヘルパー GET /mcp/logs?level=info # ログ取得 POST /mcp/restore/{backup_id} # バックアップ復元 ``` diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java index 04df5a21..8326b025 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java @@ -24,6 +24,7 @@ private void registerDefaultTools() { registerTool(new RestoreConfigTool()); registerTool(new ResendPacketTool()); registerTool(new BulkSendTool()); + registerTool(new VulCheckHelperTool()); } public void registerTool(MCPTool tool) { diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java new file mode 100644 index 00000000..8ea42a72 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java @@ -0,0 +1,553 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import packetproxy.controller.ResendController; +import packetproxy.model.OneShotPacket; +import packetproxy.model.Packet; +import packetproxy.model.Packets; +import packetproxy.common.Range; +import packetproxy.VulCheckerManager; +import packetproxy.vulchecker.VulChecker; +import packetproxy.vulchecker.generator.Generator; + +/** + * VulCheck脆弱性テストヘルパーツール + * 指定されたパケットにVulCheckテストケースを適用して連続送信を実行 + */ +public class VulCheckHelperTool extends AuthenticatedMCPTool { + + @Override + public String getName() { + return "call_vulcheck_helper"; + } + + @Override + public String getDescription() { + return "Execute VulCheck vulnerability tests with automatic payload generation and batch sending. Applies VulCheck test cases to specified packet locations and sends modified packets with configurable intervals. IMPORTANT: For precise targeting, use regex patterns with positive lookahead assertions. Examples: To target '17' in 'X-Version: 17.0.4', use pattern: '17(?=\\.0\\.4)'. To target '123' in 'userId=123&other=456', use pattern: '(?<=userId=)123(?=&)'. To target specific values while preserving structure, use patterns like: 'sessionId=\\\\w+' with replacement 'sessionId=$1'. The pattern field supports full regex syntax including lookahead/lookbehind assertions for precise matching without affecting surrounding text. Use replacement field to control how the matched text is substituted with VulCheck payloads."; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject packetIdProp = new JsonObject(); + packetIdProp.addProperty("type", "integer"); + packetIdProp.addProperty("description", "ID of the packet to use as base for VulCheck testing"); + schema.add("packet_id", packetIdProp); + + JsonObject vulCheckTypeProp = new JsonObject(); + vulCheckTypeProp.addProperty("type", "string"); + vulCheckTypeProp.addProperty("description", "Type of VulCheck to perform (Number, JWT, etc.). Use 'list' to get available types."); + schema.add("vulcheck_type", vulCheckTypeProp); + + JsonObject targetLocationsProp = new JsonObject(); + targetLocationsProp.addProperty("type", "array"); + targetLocationsProp.addProperty("description", "Array of target locations in the packet where VulCheck payloads should be injected. Can specify either regex patterns or position ranges."); + JsonObject locationItem = new JsonObject(); + locationItem.addProperty("type", "object"); + JsonObject locationProps = new JsonObject(); + + // Regex pattern approach (new) + JsonObject patternProp = new JsonObject(); + patternProp.addProperty("type", "string"); + patternProp.addProperty("description", "Regex pattern to match target locations in the packet data"); + locationProps.add("pattern", patternProp); + + JsonObject replacementProp = new JsonObject(); + replacementProp.addProperty("type", "string"); + replacementProp.addProperty("description", "Optional replacement template for pattern matches. If not specified, the entire match will be replaced."); + locationProps.add("replacement", replacementProp); + + // Position range approach (existing - for backward compatibility) + JsonObject startProp = new JsonObject(); + startProp.addProperty("type", "integer"); + startProp.addProperty("description", "Start position of the target location in the packet data (alternative to pattern)"); + locationProps.add("start", startProp); + + JsonObject endProp = new JsonObject(); + endProp.addProperty("type", "integer"); + endProp.addProperty("description", "End position of the target location in the packet data (alternative to pattern)"); + locationProps.add("end", endProp); + + JsonObject descriptionProp = new JsonObject(); + descriptionProp.addProperty("type", "string"); + descriptionProp.addProperty("description", "Optional description of this target location"); + locationProps.add("description", descriptionProp); + + locationItem.add("properties", locationProps); + targetLocationsProp.add("items", locationItem); + schema.add("target_locations", targetLocationsProp); + + JsonObject intervalProp = new JsonObject(); + intervalProp.addProperty("type", "integer"); + intervalProp.addProperty("description", "Interval between packet sends in milliseconds (default: 100)"); + intervalProp.addProperty("default", 100); + schema.add("interval_ms", intervalProp); + + JsonObject modeProp = new JsonObject(); + modeProp.addProperty("type", "string"); + JsonArray modeEnum = new JsonArray(); + modeEnum.add("sequential"); + modeEnum.add("parallel"); + modeProp.add("enum", modeEnum); + modeProp.addProperty("description", "Execution mode: sequential (with intervals) or parallel (all at once)"); + modeProp.addProperty("default", "sequential"); + schema.add("mode", modeProp); + + JsonObject maxPayloadsProp = new JsonObject(); + maxPayloadsProp.addProperty("type", "integer"); + maxPayloadsProp.addProperty("description", "Maximum number of payloads to generate per location (default: 50, max: 1000)"); + maxPayloadsProp.addProperty("default", 50); + maxPayloadsProp.addProperty("maximum", 1000); + schema.add("max_payloads", maxPayloadsProp); + + JsonObject timeoutProp = new JsonObject(); + timeoutProp.addProperty("type", "integer"); + timeoutProp.addProperty("description", "Timeout for entire operation in milliseconds (default: 300000 - 5 minutes)"); + timeoutProp.addProperty("default", 300000); + schema.add("timeout_ms", timeoutProp); + + // access_tokenを追加 + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("VulCheckHelperTool: Starting VulCheck operation"); + + // パラメータ取得 + if (!arguments.has("packet_id")) { + throw new IllegalArgumentException("packet_id parameter is required"); + } + + if (!arguments.has("vulcheck_type")) { + throw new IllegalArgumentException("vulcheck_type parameter is required"); + } + + int packetId = arguments.get("packet_id").getAsInt(); + String vulCheckType = arguments.get("vulcheck_type").getAsString(); + + // 特別なケース: 利用可能なVulCheckタイプを一覧表示 + if ("list".equals(vulCheckType)) { + return getAvailableVulCheckTypes(); + } + + if (!arguments.has("target_locations")) { + throw new IllegalArgumentException("target_locations parameter is required"); + } + + JsonArray targetLocationsJson = arguments.getAsJsonArray("target_locations"); + int intervalMs = arguments.has("interval_ms") ? arguments.get("interval_ms").getAsInt() : 100; + String mode = arguments.has("mode") ? arguments.get("mode").getAsString() : "sequential"; + int maxPayloads = arguments.has("max_payloads") ? arguments.get("max_payloads").getAsInt() : 50; + int timeoutMs = arguments.has("timeout_ms") ? arguments.get("timeout_ms").getAsInt() : 300000; + + log("VulCheckHelperTool: packet_id=" + packetId + ", vulcheck_type=" + vulCheckType + + ", locations=" + targetLocationsJson.size() + ", mode=" + mode + ", max_payloads=" + maxPayloads); + + // パケットを取得 + Packet originalPacket = Packets.getInstance().query(packetId); + if (originalPacket == null) { + throw new IllegalArgumentException("Packet with ID " + packetId + " not found"); + } + + // OneShotPacketを作成 + OneShotPacket originalOneShot; + if (originalPacket.getModifiedData().length > 0) { + originalOneShot = originalPacket.getOneShotFromModifiedData(); + } else if (originalPacket.getSentData().length > 0) { + originalOneShot = originalPacket.getOneShotPacket(originalPacket.getSentData()); + } else { + originalOneShot = originalPacket.getOneShotFromDecodedData(); + } + + if (originalOneShot == null) { + throw new IllegalArgumentException("Cannot create OneShotPacket from packet ID " + packetId); + } + + // VulCheckerを取得 + VulChecker vulChecker = VulCheckerManager.getInstance().createInstance(vulCheckType); + if (vulChecker == null) { + throw new IllegalArgumentException("VulCheck type '" + vulCheckType + "' not found. Use 'list' to see available types."); + } + + // ターゲット位置を解析 + List targetLocations = parseTargetLocations(targetLocationsJson, originalOneShot.getData()); + + long startTime = System.currentTimeMillis(); + + // VulCheckテストを実行 + VulCheckResult result = executeVulCheckTests(originalOneShot, vulChecker, targetLocations, + intervalMs, mode, maxPayloads, timeoutMs); + + long executionTime = System.currentTimeMillis() - startTime; + + // 結果を構築 + JsonObject response = new JsonObject(); + response.addProperty("success", result.overallSuccess); + response.addProperty("vulcheck_type", vulCheckType); + response.addProperty("mode", mode); + response.addProperty("total_locations", targetLocations.size()); + response.addProperty("total_payloads_generated", result.totalPayloadsGenerated); + response.addProperty("total_packets_sent", result.totalPacketsSent); + response.addProperty("total_failed", result.totalFailed); + response.addProperty("execution_time_ms", executionTime); + + // 各ターゲット位置の結果 + JsonArray locationResults = new JsonArray(); + for (LocationResult locResult : result.locationResults) { + JsonObject locJson = new JsonObject(); + locJson.addProperty("start", locResult.range.getPositionStart()); + locJson.addProperty("end", locResult.range.getPositionEnd()); + locJson.addProperty("description", locResult.description); + locJson.addProperty("payloads_generated", locResult.payloadsGenerated); + locJson.addProperty("packets_sent", locResult.packetsSent); + locJson.addProperty("packets_failed", locResult.packetsFailed); + locJson.addProperty("execution_time_ms", locResult.executionTimeMs); + + // 生成されたペイロードの一覧 + JsonArray payloadsList = new JsonArray(); + for (String payload : locResult.generatedPayloads) { + payloadsList.add(payload); + } + locJson.add("generated_payloads", payloadsList); + + locationResults.add(locJson); + } + response.add("location_results", locationResults); + + // パフォーマンス統計 + JsonObject performance = new JsonObject(); + performance.addProperty("average_interval_ms", result.averageIntervalMs); + performance.addProperty("payloads_per_second", (double) result.totalPacketsSent / (executionTime / 1000.0)); + performance.addProperty("success_rate_percent", result.totalPacketsSent > 0 + ? ((double)(result.totalPacketsSent - result.totalFailed) / result.totalPacketsSent) * 100.0 : 0.0); + response.add("performance", performance); + + log("VulCheckHelperTool: Completed. Generated " + result.totalPayloadsGenerated + " payloads, sent " + + result.totalPacketsSent + " packets, " + result.totalFailed + " failed, time: " + executionTime + "ms"); + + return response; + } + + /** + * 利用可能なVulCheckタイプを取得 + */ + private JsonObject getAvailableVulCheckTypes() throws Exception { + VulCheckerManager manager = VulCheckerManager.getInstance(); + String[] vulCheckerNames = manager.getVulCheckerNameList(); + + JsonObject result = new JsonObject(); + result.addProperty("available_vulcheck_types", String.join(", ", vulCheckerNames)); + + JsonArray typesArray = new JsonArray(); + Map typeDetails = new HashMap<>(); + + for (String name : vulCheckerNames) { + VulChecker checker = manager.createInstance(name); + if (checker != null) { + JsonObject typeInfo = new JsonObject(); + typeInfo.addProperty("name", name); + typeInfo.addProperty("description", "VulCheck tests for " + name + " vulnerabilities"); + + // ジェネレータの詳細を追加 + ImmutableList generators = checker.getGenerators(); + JsonArray generatorsList = new JsonArray(); + for (Generator gen : generators) { + JsonObject genInfo = new JsonObject(); + genInfo.addProperty("name", gen.getName()); + genInfo.addProperty("generate_on_start", gen.generateOnStart()); + generatorsList.add(genInfo); + } + typeInfo.add("generators", generatorsList); + typeInfo.addProperty("generator_count", generators.size()); + + typesArray.add(typeInfo); + typeDetails.put(name, typeInfo); + } + } + + result.add("vulcheck_types", typesArray); + result.addProperty("total_types", vulCheckerNames.length); + + return result; + } + + /** + * ターゲット位置を解析してTargetLocationリストに変換 + */ + private List parseTargetLocations(JsonArray targetLocations, byte[] packetData) throws Exception { + List locations = new ArrayList<>(); + String packetStr = new String(packetData); + + for (JsonElement element : targetLocations) { + JsonObject location = element.getAsJsonObject(); + + // Check if regex pattern is specified + if (location.has("pattern")) { + // Regex-based approach + String pattern = location.get("pattern").getAsString(); + String replacement = location.has("replacement") ? location.get("replacement").getAsString() : null; + String description = location.has("description") ? location.get("description").getAsString() : ("Pattern: " + pattern); + + // Find all matches for the pattern + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(packetStr); + + int matchCount = 0; + while (matcher.find()) { + matchCount++; + int start = matcher.start(); + int end = matcher.end(); + + TargetLocation targetLoc = new TargetLocation(); + targetLoc.range = Range.of(start, end); + targetLoc.description = description + " (match " + matchCount + ")"; + targetLoc.pattern = pattern; + targetLoc.replacement = replacement; + targetLoc.originalMatch = matcher.group(); + + locations.add(targetLoc); + } + + if (matchCount == 0) { + log("VulCheckHelperTool: No matches found for pattern: " + pattern); + } + } else if (location.has("start") && location.has("end")) { + // Position-based approach (backward compatibility) + int start = location.get("start").getAsInt(); + int end = location.get("end").getAsInt(); + String description = location.has("description") ? location.get("description").getAsString() : ("Range " + start + "-" + end); + + if (start < 0 || end < start || end > packetData.length) { + throw new IllegalArgumentException("Invalid range: start=" + start + ", end=" + end + ", packet length=" + packetData.length); + } + + TargetLocation targetLoc = new TargetLocation(); + targetLoc.range = Range.of(start, end); + targetLoc.description = description; + targetLoc.originalMatch = packetStr.substring(start, end); + + locations.add(targetLoc); + } else { + throw new IllegalArgumentException("Each target location must have either 'pattern' or both 'start' and 'end' properties"); + } + } + + if (locations.isEmpty()) { + throw new IllegalArgumentException("No valid target locations found"); + } + + return locations; + } + + /** + * VulCheckテストを実行 + */ + private VulCheckResult executeVulCheckTests(OneShotPacket originalPacket, VulChecker vulChecker, + List targetLocations, int intervalMs, String mode, int maxPayloads, int timeoutMs) throws Exception { + + VulCheckResult result = new VulCheckResult(); + result.overallSuccess = true; + + // 各ターゲット位置に対してテストを実行 + for (int locationIndex = 0; locationIndex < targetLocations.size(); locationIndex++) { + TargetLocation targetLocation = targetLocations.get(locationIndex); + + log("VulCheckHelperTool: Processing target location " + (locationIndex + 1) + "/" + targetLocations.size() + + " at range " + targetLocation.range.getPositionStart() + "-" + targetLocation.range.getPositionEnd() + + " (" + targetLocation.description + ")"); + + LocationResult locResult = processTargetLocation(originalPacket, vulChecker, targetLocation, + intervalMs, mode, maxPayloads); + + result.locationResults.add(locResult); + result.totalPayloadsGenerated += locResult.payloadsGenerated; + result.totalPacketsSent += locResult.packetsSent; + result.totalFailed += locResult.packetsFailed; + + if (locResult.packetsFailed > 0) { + result.overallSuccess = false; + } + } + + // 平均間隔を計算 + if (result.totalPacketsSent > 1) { + result.averageIntervalMs = intervalMs; // sequentialモードの場合 + } + + return result; + } + + /** + * 特定のターゲット位置でVulCheckテストを実行 + */ + private LocationResult processTargetLocation(OneShotPacket originalPacket, VulChecker vulChecker, + TargetLocation targetLocation, int intervalMs, String mode, int maxPayloads) throws Exception { + + LocationResult result = new LocationResult(); + result.range = targetLocation.range; + result.description = targetLocation.description; + + long startTime = System.currentTimeMillis(); + + // 元のパケットデータを取得 + byte[] originalData = originalPacket.getData(); + String originalText = new String(originalData); + + // ターゲット範囲のデータを取得 + if (targetLocation.range.getPositionEnd() > originalData.length) { + throw new IllegalArgumentException("Target range exceeds packet data length: range end=" + + targetLocation.range.getPositionEnd() + ", data length=" + originalData.length); + } + + String targetData = targetLocation.originalMatch != null ? targetLocation.originalMatch + : originalText.substring(targetLocation.range.getPositionStart(), targetLocation.range.getPositionEnd()); + + // VulCheckのジェネレータを取得してペイロードを生成 + ImmutableList generators = vulChecker.getGenerators(); + ResendController resendController = ResendController.getInstance(); + + int payloadCount = 0; + int sentCount = 0; + int failedCount = 0; + + for (Generator generator : generators) { + if (payloadCount >= maxPayloads) { + log("VulCheckHelperTool: Reached max payload limit (" + maxPayloads + ")"); + break; + } + + try { + String payload = generator.generate(targetData); + if (payload != null && !payload.equals(targetData)) { + result.generatedPayloads.add(payload); + payloadCount++; + + // パケットを改変して送信 + String modifiedText; + if (targetLocation.pattern != null && targetLocation.replacement != null) { + // Regex pattern replacement approach + String replacementTemplate = targetLocation.replacement.replace("$1", payload); + Pattern pattern = Pattern.compile(targetLocation.pattern); + Matcher matcher = pattern.matcher(originalText); + + // Find the specific match we're targeting + StringBuffer sb = new StringBuffer(); + int currentMatch = 0; + while (matcher.find()) { + if (matcher.start() == targetLocation.range.getPositionStart()) { + // This is our target match + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacementTemplate)); + break; + } else { + // Keep other matches unchanged + matcher.appendReplacement(sb, Matcher.quoteReplacement(matcher.group())); + } + } + matcher.appendTail(sb); + modifiedText = sb.toString(); + } else { + // Position-based replacement approach + modifiedText = originalText.substring(0, targetLocation.range.getPositionStart()) + + payload + + originalText.substring(targetLocation.range.getPositionEnd()); + } + + byte[] modifiedData = modifiedText.getBytes(); + + OneShotPacket modifiedPacket = new OneShotPacket(originalPacket.getId(), + originalPacket.getListenPort(), originalPacket.getClient(), originalPacket.getServer(), + originalPacket.getServerName(), originalPacket.getUseSSL(), modifiedData, + originalPacket.getEncoder(), originalPacket.getAlpn(), originalPacket.getDirection(), + originalPacket.getConn(), originalPacket.getGroup()); + + // 送信モードに応じて処理 + if ("parallel".equals(mode)) { + // 並列送信 - すぐに送信 + try { + resendController.resend(modifiedPacket); + sentCount++; + } catch (Exception e) { + log("VulCheckHelperTool: Failed to send packet with payload: " + e.getMessage()); + failedCount++; + } + } else { + // 順次送信 - 間隔を設けて送信 + try { + resendController.resend(modifiedPacket); + sentCount++; + + if (intervalMs > 0 && payloadCount < maxPayloads && payloadCount < generators.size()) { + Thread.sleep(intervalMs); + } + } catch (Exception e) { + log("VulCheckHelperTool: Failed to send packet with payload: " + e.getMessage()); + failedCount++; + } + } + } + } catch (Exception e) { + log("VulCheckHelperTool: Failed to generate payload with generator " + generator.getName() + ": " + e.getMessage()); + failedCount++; + } + } + + result.payloadsGenerated = payloadCount; + result.packetsSent = sentCount; + result.packetsFailed = failedCount; + result.executionTimeMs = System.currentTimeMillis() - startTime; + + log("VulCheckHelperTool: Location complete - generated " + payloadCount + " payloads, sent " + + sentCount + " packets, " + failedCount + " failed"); + + return result; + } + + /** + * VulCheckテストの全体結果 + */ + private static class VulCheckResult { + boolean overallSuccess = true; + int totalPayloadsGenerated = 0; + int totalPacketsSent = 0; + int totalFailed = 0; + double averageIntervalMs = 0; + List locationResults = new ArrayList<>(); + } + + /** + * ターゲット位置の情報 + */ + private static class TargetLocation { + Range range; + String description; + String pattern; // regex pattern (if used) + String replacement; // replacement template (if used) + String originalMatch; // original matched text + } + + /** + * 特定位置でのテスト結果 + */ + private static class LocationResult { + Range range; + String description; + int payloadsGenerated = 0; + int packetsSent = 0; + int packetsFailed = 0; + long executionTimeMs = 0; + List generatedPayloads = new ArrayList<>(); + } +} \ No newline at end of file From 3863d758ca9292fb1f5d055d42b4a07e6e7f0ac7 Mon Sep 17 00:00:00 2001 From: kakira Date: Sun, 10 Aug 2025 23:33:59 +0900 Subject: [PATCH 29/39] =?UTF-8?q?=E3=83=9A=E3=82=A2=E3=81=AE=E3=83=91?= =?UTF-8?q?=E3=82=B1=E3=83=83=E3=83=88=E6=83=85=E5=A0=B1=E3=82=92=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-spec.md | 107 ++++++++++++++---- .../mcp/tools/PacketDetailTool.java | 94 ++++++++++++++- 2 files changed, 179 insertions(+), 22 deletions(-) diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index 34d078c1..ec0979a4 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -120,7 +120,7 @@ PacketProxyのパケット履歴を検索・取得します。フィルタリン ### 2. `get_packet_detail` - パケット詳細取得 -特定のパケットの詳細情報を取得します。 +特定のパケットの詳細情報を取得します。指定したパケットIDがリクエストの場合は対応するレスポンスも、レスポンスの場合は対応するリクエストも同時に返します。ペア取得機能は`include_pair`オプションで制御できます。 **リクエスト:** @@ -133,7 +133,8 @@ PacketProxyのパケット履歴を検索・取得します。フィルタリン "arguments": { "access_token": "your_access_token_here", "packet_id": 123, - "include_body": true + "include_body": true, + "include_pair": true } }, "id": 2 @@ -142,42 +143,108 @@ PacketProxyのパケット履歴を検索・取得します。フィルタリン **パラメータ:** - `access_token` (string, required): PacketProxy設定のアクセストークン -- `packet_id` (number, required): パケットID +- `packet_id` (number, required): パケットID(リクエストまたはレスポンスのどちらでも指定可能) - `include_body` (boolean, optional): リクエスト/レスポンスボディを含める (デフォルト: false) +- `include_pair` (boolean, optional): ペアパケット(リクエスト指定時はレスポンス、レスポンス指定時はリクエスト)を含める (デフォルト: true) -**レスポンス:** +**レスポンス(ペアが見つかった場合):** ```json { "jsonrpc": "2.0", "result": { - "id": 123, - "method": "GET", - "url": "/api/users", - "status": 200, - "headers": { - "request": [ + "paired": true, + "requested_packet_id": 123, + "group": 1001, + "conn": 5, + "request": { + "id": 123, + "direction": "client", + "method": "GET", + "url": "/api/users", + "version": "HTTP/1.1", + "headers": [ {"name": "Host", "value": "api.example.com"}, {"name": "User-Agent", "value": "Mozilla/5.0..."} ], - "response": [ + "body": "", + "length": 256, + "time": "2025-01-15T10:30:00Z", + "resend": false, + "modified": false, + "type": "HTTP", + "encode": "HTTP", + "client": {"ip": "192.168.1.100", "port": 54321}, + "server": {"ip": "192.168.1.1", "port": 80} + }, + "response": { + "id": 124, + "direction": "server", + "status": 200, + "status_text": "OK", + "headers": [ {"name": "Content-Type", "value": "application/json"}, {"name": "Content-Length", "value": "1024"} - ] - }, - "body": { - "request": "", - "response": "{\"users\": [...]}" - }, - "timing": { - "timestamp": "2025-01-15T10:30:00Z", - "duration_ms": 245 + ], + "body": "{\"users\": [...]}", + "length": 1024, + "time": "2025-01-15T10:30:01Z", + "resend": false, + "modified": false, + "type": "HTTP", + "encode": "HTTP", + "client": {"ip": "192.168.1.100", "port": 54321}, + "server": {"ip": "192.168.1.1", "port": 80} } }, "id": 2 } ``` +**レスポンス(ペアが見つからない場合):** + +```json +{ + "jsonrpc": "2.0", + "result": { + "paired": false, + "requested_packet_id": 123, + "group": 1001, + "conn": 5, + "request": { + "id": 123, + "direction": "client", + "method": "GET", + "url": "/api/users", + "version": "HTTP/1.1", + "headers": [ + {"name": "Host", "value": "api.example.com"}, + {"name": "User-Agent", "value": "Mozilla/5.0..."} + ], + "body": "", + "length": 256, + "time": "2025-01-15T10:30:00Z", + "resend": false, + "modified": false, + "type": "HTTP", + "encode": "HTTP", + "client": {"ip": "192.168.1.100", "port": 54321}, + "server": {"ip": "192.168.1.1", "port": 80} + }, + "response": null + }, + "id": 2 +} +``` + +**主な機能:** +- **ペア検索機能**: 指定されたパケットに対応するリクエスト/レスポンスを自動的に検索(`include_pair=true`の場合) +- **統一レスポンス形式**: リクエストIDを指定してもレスポンスIDを指定しても、同じ形式でリクエスト/レスポンス両方の詳細を返す +- **ペア情報**: `paired`フィールドでペアが見つかったかどうかを示す +- **詳細情報の追加**: 各パケットに`direction`(client/server)フィールドを追加 +- **接続情報**: `group`と`conn`フィールドでパケット間の関連性を示す +- **ペア取得制御**: `include_pair=false`を指定することで、指定したパケットのみを取得可能 + ### 3. `get_logs` - ログ取得 PacketProxyのログを取得します。 diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java index 13f92bc8..18ac699a 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java @@ -7,6 +7,7 @@ import com.google.gson.JsonObject; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; +import java.util.List; import packetproxy.model.Packet; import packetproxy.model.Packets; @@ -40,6 +41,12 @@ public JsonObject getInputSchema() { includeBodyProp.addProperty("default", false); schema.add("include_body", includeBodyProp); + JsonObject includePairProp = new JsonObject(); + includePairProp.addProperty("type", "boolean"); + includePairProp.addProperty("description", "Whether to include paired packet (request when response specified, response when request specified)"); + includePairProp.addProperty("default", true); + schema.add("include_pair", includePairProp); + return addAccessTokenToSchema(schema); } @@ -53,6 +60,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception int packetId = arguments.get("packet_id").getAsInt(); boolean includeBody = arguments.has("include_body") && arguments.get("include_body").getAsBoolean(); + boolean includePair = !arguments.has("include_pair") || arguments.get("include_pair").getAsBoolean(); try { Packets packets = Packets.getInstance(); @@ -62,7 +70,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception throw new Exception("Packet not found: " + packetId); } - JsonObject data = buildPacketDetail(packet, includeBody); + JsonObject data = buildPacketDetail(packet, includeBody, includePair); JsonObject content = new JsonObject(); content.addProperty("type", "text"); @@ -83,7 +91,88 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception } } - private JsonObject buildPacketDetail(Packet packet, boolean includeBody) throws Exception { + private JsonObject buildPacketDetail(Packet packet, boolean includeBody, boolean includePair) throws Exception { + JsonObject result = new JsonObject(); + + if (includePair) { + // Try to find the paired packet (request/response) + Packet pairedPacket = findPairedPacket(packet); + + if (pairedPacket != null) { + // Build paired request/response structure + Packet requestPacket = (packet.getDirection() == Packet.Direction.CLIENT) ? packet : pairedPacket; + Packet responsePacket = (packet.getDirection() == Packet.Direction.SERVER) ? packet : pairedPacket; + + // Request details + JsonObject request = buildSinglePacketDetail(requestPacket, includeBody, "request"); + result.add("request", request); + + // Response details + JsonObject response = buildSinglePacketDetail(responsePacket, includeBody, "response"); + result.add("response", response); + + // Add pairing information + result.addProperty("paired", true); + result.addProperty("requested_packet_id", packet.getId()); + result.addProperty("group", packet.getGroup()); + result.addProperty("conn", packet.getConn()); + } else { + // Single packet (no pair found) + JsonObject singlePacket = buildSinglePacketDetail(packet, includeBody, packet.getDirection() == Packet.Direction.CLIENT ? "request" : "response"); + if (packet.getDirection() == Packet.Direction.CLIENT) { + result.add("request", singlePacket); + result.add("response", null); + } else { + result.add("request", null); + result.add("response", singlePacket); + } + result.addProperty("paired", false); + result.addProperty("requested_packet_id", packet.getId()); + result.addProperty("group", packet.getGroup()); + result.addProperty("conn", packet.getConn()); + } + } else { + // Return only the requested packet + JsonObject singlePacket = buildSinglePacketDetail(packet, includeBody, packet.getDirection() == Packet.Direction.CLIENT ? "request" : "response"); + if (packet.getDirection() == Packet.Direction.CLIENT) { + result.add("request", singlePacket); + result.add("response", null); + } else { + result.add("request", null); + result.add("response", singlePacket); + } + result.addProperty("paired", false); + result.addProperty("requested_packet_id", packet.getId()); + result.addProperty("group", packet.getGroup()); + result.addProperty("conn", packet.getConn()); + } + + return result; + } + + private Packet findPairedPacket(Packet packet) throws Exception { + Packets packets = Packets.getInstance(); + + // Look for a packet with same group and conn but opposite direction + Packet.Direction targetDirection = (packet.getDirection() == Packet.Direction.CLIENT) + ? Packet.Direction.SERVER : Packet.Direction.CLIENT; + + // Search through packets with same group + // Note: This is a simple implementation. In a real system, you might want to + // add specific query methods to Packets class for better performance + List allPackets = packets.queryAll(); + for (Packet p : allPackets) { + if (p.getGroup() == packet.getGroup() && + p.getConn() == packet.getConn() && + p.getDirection() == targetDirection && + p.getId() != packet.getId()) { + return p; + } + } + return null; + } + + private JsonObject buildSinglePacketDetail(Packet packet, boolean includeBody, String type) throws Exception { JsonObject result = new JsonObject(); // Basic packet info @@ -94,6 +183,7 @@ private JsonObject buildPacketDetail(Packet packet, boolean includeBody) throws result.addProperty("modified", packet.getModified()); result.addProperty("type", packet.getContentType()); result.addProperty("encode", packet.getEncoder()); + result.addProperty("direction", packet.getDirection().toString().toLowerCase()); // Client/Server info JsonObject client = new JsonObject(); From 8f081eb1c59274986bf01675c822937eae94e7c1 Mon Sep 17 00:00:00 2001 From: kakira Date: Sun, 10 Aug 2025 23:43:13 +0900 Subject: [PATCH 30/39] spotlessApply --- doc/mcp-server-spec.md | 24 +- .../mcp/tools/AuthenticatedMCPTool.java | 5 +- .../mcp/tools/PacketDetailTool.java | 38 ++-- .../mcp/tools/RestoreConfigTool.java | 7 +- .../mcp/tools/UpdateConfigTool.java | 10 +- .../mcp/tools/VulCheckHelperTool.java | 215 ++++++++++-------- 6 files changed, 164 insertions(+), 135 deletions(-) diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index ec0979a4..c6e7a2f9 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -423,10 +423,10 @@ IMPORTANT: Requires a complete configuration object, not partial updates. **完全な設定オブジェクトが必要:** - `config_json`は部分的な設定ではなく、**完全な設定オブジェクト**である必要があります - 以下の配列は必須です(空配列でも可): - - `listenPorts`: リッスンポート設定 - - `servers`: サーバー設定 - - `modifications`: 改変設定 - - `sslPassThroughs`: SSL パススルー設定 +- `listenPorts`: リッスンポート設定 +- `servers`: サーバー設定 +- `modifications`: 改変設定 +- `sslPassThroughs`: SSL パススルー設定 - 部分的な設定を渡すとnull pointerエラーが発生します **推奨ワークフロー:** @@ -768,14 +768,14 @@ IMPORTANT: Requires a complete configuration object, not partial updates. - `packet_id` (number, required): VulCheckテストのベースとして使用するパケットID - `vulcheck_type` (string, required): 実行するVulCheckの種類 (Number, JWT等)。'list'を指定すると利用可能なタイプ一覧を取得 - `target_locations` (array, required): VulCheckペイロードを注入するパケット内の対象位置の配列。正規表現パターンまたは位置範囲で指定可能 - - **正規表現アプローチ (推奨):** - - `pattern` (string, required): マッチ対象の正規表現パターン (例: `"sessionId=\\w+"`) - - `replacement` (string, optional): 置換テンプレート。省略時はマッチ全体を置換。`$1`でペイロードを表す (例: `"sessionId=$1"`) - - `description` (string, optional): この対象位置の説明 - - **位置範囲アプローチ (後方互換性のため保持):** - - `start` (number, required): 対象位置の開始位置 - - `end` (number, required): 対象位置の終了位置 - - `description` (string, optional): この対象位置の説明 +- **正規表現アプローチ (推奨):** +- `pattern` (string, required): マッチ対象の正規表現パターン (例: `"sessionId=\\w+"`) +- `replacement` (string, optional): 置換テンプレート。省略時はマッチ全体を置換。`$1`でペイロードを表す (例: `"sessionId=$1"`) +- `description` (string, optional): この対象位置の説明 +- **位置範囲アプローチ (後方互換性のため保持):** +- `start` (number, required): 対象位置の開始位置 +- `end` (number, required): 対象位置の終了位置 +- `description` (string, optional): この対象位置の説明 - `interval_ms` (number, optional): パケット送信間隔(ms) (デフォルト: 100) - `mode` (string, optional): 実行モード "sequential" | "parallel" (デフォルト: "sequential") - `max_payloads` (number, optional): 位置ごとの最大ペイロード生成数 (デフォルト: 50, 最大: 1000) diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java index 4120bacb..20abe97e 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java @@ -25,7 +25,7 @@ protected void validateAccessToken(JsonObject arguments) throws Exception { throw new Exception( "access_token parameter is required. Please provide your PacketProxy access token from Settings or leave empty (\"\") to use environment variable."); } - + // 空文字列の場合は環境変数から取得する想定なのでvalidationをスキップ if (providedToken.trim().isEmpty()) { log("Empty access_token provided, assuming environment variable usage"); @@ -66,7 +66,8 @@ protected String getConfiguredAccessToken() throws Exception { protected JsonObject addAccessTokenToSchema(JsonObject schema) { JsonObject accessTokenProp = new JsonObject(); accessTokenProp.addProperty("type", "string"); - accessTokenProp.addProperty("description", "Access token for authentication. Leave empty (\"\") to use environment variable (handled by scripts/mcp-http-bridge.js), or provide explicit token string"); + accessTokenProp.addProperty("description", + "Access token for authentication. Leave empty (\"\") to use environment variable (handled by scripts/mcp-http-bridge.js), or provide explicit token string"); schema.add("access_token", accessTokenProp); return schema; } diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java index 18ac699a..1cbb15ed 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java @@ -43,7 +43,8 @@ public JsonObject getInputSchema() { JsonObject includePairProp = new JsonObject(); includePairProp.addProperty("type", "boolean"); - includePairProp.addProperty("description", "Whether to include paired packet (request when response specified, response when request specified)"); + includePairProp.addProperty("description", + "Whether to include paired packet (request when response specified, response when request specified)"); includePairProp.addProperty("default", true); schema.add("include_pair", includePairProp); @@ -93,24 +94,24 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception private JsonObject buildPacketDetail(Packet packet, boolean includeBody, boolean includePair) throws Exception { JsonObject result = new JsonObject(); - + if (includePair) { // Try to find the paired packet (request/response) Packet pairedPacket = findPairedPacket(packet); - + if (pairedPacket != null) { // Build paired request/response structure Packet requestPacket = (packet.getDirection() == Packet.Direction.CLIENT) ? packet : pairedPacket; Packet responsePacket = (packet.getDirection() == Packet.Direction.SERVER) ? packet : pairedPacket; - + // Request details JsonObject request = buildSinglePacketDetail(requestPacket, includeBody, "request"); result.add("request", request); - + // Response details JsonObject response = buildSinglePacketDetail(responsePacket, includeBody, "response"); result.add("response", response); - + // Add pairing information result.addProperty("paired", true); result.addProperty("requested_packet_id", packet.getId()); @@ -118,7 +119,8 @@ private JsonObject buildPacketDetail(Packet packet, boolean includeBody, boolean result.addProperty("conn", packet.getConn()); } else { // Single packet (no pair found) - JsonObject singlePacket = buildSinglePacketDetail(packet, includeBody, packet.getDirection() == Packet.Direction.CLIENT ? "request" : "response"); + JsonObject singlePacket = buildSinglePacketDetail(packet, includeBody, + packet.getDirection() == Packet.Direction.CLIENT ? "request" : "response"); if (packet.getDirection() == Packet.Direction.CLIENT) { result.add("request", singlePacket); result.add("response", null); @@ -133,7 +135,8 @@ private JsonObject buildPacketDetail(Packet packet, boolean includeBody, boolean } } else { // Return only the requested packet - JsonObject singlePacket = buildSinglePacketDetail(packet, includeBody, packet.getDirection() == Packet.Direction.CLIENT ? "request" : "response"); + JsonObject singlePacket = buildSinglePacketDetail(packet, includeBody, + packet.getDirection() == Packet.Direction.CLIENT ? "request" : "response"); if (packet.getDirection() == Packet.Direction.CLIENT) { result.add("request", singlePacket); result.add("response", null); @@ -149,29 +152,28 @@ private JsonObject buildPacketDetail(Packet packet, boolean includeBody, boolean return result; } - + private Packet findPairedPacket(Packet packet) throws Exception { Packets packets = Packets.getInstance(); - + // Look for a packet with same group and conn but opposite direction - Packet.Direction targetDirection = (packet.getDirection() == Packet.Direction.CLIENT) - ? Packet.Direction.SERVER : Packet.Direction.CLIENT; - + Packet.Direction targetDirection = (packet.getDirection() == Packet.Direction.CLIENT) + ? Packet.Direction.SERVER + : Packet.Direction.CLIENT; + // Search through packets with same group // Note: This is a simple implementation. In a real system, you might want to // add specific query methods to Packets class for better performance List allPackets = packets.queryAll(); for (Packet p : allPackets) { - if (p.getGroup() == packet.getGroup() && - p.getConn() == packet.getConn() && - p.getDirection() == targetDirection && - p.getId() != packet.getId()) { + if (p.getGroup() == packet.getGroup() && p.getConn() == packet.getConn() + && p.getDirection() == targetDirection && p.getId() != packet.getId()) { return p; } } return null; } - + private JsonObject buildSinglePacketDetail(Packet packet, boolean includeBody, String type) throws Exception { JsonObject result = new JsonObject(); diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java index 341ab728..56d2e739 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java @@ -34,7 +34,8 @@ public JsonObject getInputSchema() { JsonObject suppressDialogProp = new JsonObject(); suppressDialogProp.addProperty("type", "boolean"); - suppressDialogProp.addProperty("description", "Suppress confirmation dialog for configuration restore (default: false)"); + suppressDialogProp.addProperty("description", + "Suppress confirmation dialog for configuration restore (default: false)"); suppressDialogProp.addProperty("default", false); schema.add("suppress_dialog", suppressDialogProp); @@ -50,7 +51,9 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception } String backupId = arguments.get("backup_id").getAsString(); - boolean suppressDialog = arguments.has("suppress_dialog") ? arguments.get("suppress_dialog").getAsBoolean() : false; + boolean suppressDialog = arguments.has("suppress_dialog") + ? arguments.get("suppress_dialog").getAsBoolean() + : false; try { log("RestoreConfigTool step 1: Loading backup configuration"); diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java index f87e1296..bec2565a 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java @@ -37,7 +37,8 @@ public JsonObject getInputSchema() { JsonObject configJsonProp = new JsonObject(); configJsonProp.addProperty("type", "object"); - configJsonProp.addProperty("description", "PacketProxyHub-compatible configuration JSON containing COMPLETE configuration object. Must include all required arrays: listenPorts, servers, modifications, sslPassThroughs (can be empty arrays). Partial configurations will cause null pointer errors. Recommended workflow: 1) Call get_config() first, 2) Modify specific fields in the returned object, 3) Pass the entire modified object here."); + configJsonProp.addProperty("description", + "PacketProxyHub-compatible configuration JSON containing COMPLETE configuration object. Must include all required arrays: listenPorts, servers, modifications, sslPassThroughs (can be empty arrays). Partial configurations will cause null pointer errors. Recommended workflow: 1) Call get_config() first, 2) Modify specific fields in the returned object, 3) Pass the entire modified object here."); schema.add("config_json", configJsonProp); JsonObject backupProp = new JsonObject(); @@ -48,7 +49,8 @@ public JsonObject getInputSchema() { JsonObject suppressDialogProp = new JsonObject(); suppressDialogProp.addProperty("type", "boolean"); - suppressDialogProp.addProperty("description", "Suppress confirmation dialog for configuration update (default: false)"); + suppressDialogProp.addProperty("description", + "Suppress confirmation dialog for configuration update (default: false)"); suppressDialogProp.addProperty("default", false); schema.add("suppress_dialog", suppressDialogProp); @@ -65,7 +67,9 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception JsonObject configJson = arguments.getAsJsonObject("config_json"); boolean backup = arguments.has("backup") ? arguments.get("backup").getAsBoolean() : true; - boolean suppressDialog = arguments.has("suppress_dialog") ? arguments.get("suppress_dialog").getAsBoolean() : false; + boolean suppressDialog = arguments.has("suppress_dialog") + ? arguments.get("suppress_dialog").getAsBoolean() + : false; try { log("UpdateConfigTool step 1: Starting configuration update"); diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java index 8ea42a72..aef1b18d 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java @@ -2,28 +2,27 @@ import static packetproxy.util.Logging.log; +import com.google.common.collect.ImmutableList; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.common.collect.ImmutableList; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import packetproxy.VulCheckerManager; +import packetproxy.common.Range; import packetproxy.controller.ResendController; import packetproxy.model.OneShotPacket; import packetproxy.model.Packet; import packetproxy.model.Packets; -import packetproxy.common.Range; -import packetproxy.VulCheckerManager; import packetproxy.vulchecker.VulChecker; import packetproxy.vulchecker.generator.Generator; /** - * VulCheck脆弱性テストヘルパーツール - * 指定されたパケットにVulCheckテストケースを適用して連続送信を実行 + * VulCheck脆弱性テストヘルパーツール 指定されたパケットにVulCheckテストケースを適用して連続送信を実行 */ public class VulCheckHelperTool extends AuthenticatedMCPTool { @@ -48,12 +47,14 @@ public JsonObject getInputSchema() { JsonObject vulCheckTypeProp = new JsonObject(); vulCheckTypeProp.addProperty("type", "string"); - vulCheckTypeProp.addProperty("description", "Type of VulCheck to perform (Number, JWT, etc.). Use 'list' to get available types."); + vulCheckTypeProp.addProperty("description", + "Type of VulCheck to perform (Number, JWT, etc.). Use 'list' to get available types."); schema.add("vulcheck_type", vulCheckTypeProp); JsonObject targetLocationsProp = new JsonObject(); targetLocationsProp.addProperty("type", "array"); - targetLocationsProp.addProperty("description", "Array of target locations in the packet where VulCheck payloads should be injected. Can specify either regex patterns or position ranges."); + targetLocationsProp.addProperty("description", + "Array of target locations in the packet where VulCheck payloads should be injected. Can specify either regex patterns or position ranges."); JsonObject locationItem = new JsonObject(); locationItem.addProperty("type", "object"); JsonObject locationProps = new JsonObject(); @@ -66,18 +67,21 @@ public JsonObject getInputSchema() { JsonObject replacementProp = new JsonObject(); replacementProp.addProperty("type", "string"); - replacementProp.addProperty("description", "Optional replacement template for pattern matches. If not specified, the entire match will be replaced."); + replacementProp.addProperty("description", + "Optional replacement template for pattern matches. If not specified, the entire match will be replaced."); locationProps.add("replacement", replacementProp); // Position range approach (existing - for backward compatibility) JsonObject startProp = new JsonObject(); startProp.addProperty("type", "integer"); - startProp.addProperty("description", "Start position of the target location in the packet data (alternative to pattern)"); + startProp.addProperty("description", + "Start position of the target location in the packet data (alternative to pattern)"); locationProps.add("start", startProp); JsonObject endProp = new JsonObject(); endProp.addProperty("type", "integer"); - endProp.addProperty("description", "End position of the target location in the packet data (alternative to pattern)"); + endProp.addProperty("description", + "End position of the target location in the packet data (alternative to pattern)"); locationProps.add("end", endProp); JsonObject descriptionProp = new JsonObject(); @@ -107,14 +111,16 @@ public JsonObject getInputSchema() { JsonObject maxPayloadsProp = new JsonObject(); maxPayloadsProp.addProperty("type", "integer"); - maxPayloadsProp.addProperty("description", "Maximum number of payloads to generate per location (default: 50, max: 1000)"); + maxPayloadsProp.addProperty("description", + "Maximum number of payloads to generate per location (default: 50, max: 1000)"); maxPayloadsProp.addProperty("default", 50); maxPayloadsProp.addProperty("maximum", 1000); schema.add("max_payloads", maxPayloadsProp); JsonObject timeoutProp = new JsonObject(); timeoutProp.addProperty("type", "integer"); - timeoutProp.addProperty("description", "Timeout for entire operation in milliseconds (default: 300000 - 5 minutes)"); + timeoutProp.addProperty("description", + "Timeout for entire operation in milliseconds (default: 300000 - 5 minutes)"); timeoutProp.addProperty("default", 300000); schema.add("timeout_ms", timeoutProp); @@ -153,8 +159,8 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception int maxPayloads = arguments.has("max_payloads") ? arguments.get("max_payloads").getAsInt() : 50; int timeoutMs = arguments.has("timeout_ms") ? arguments.get("timeout_ms").getAsInt() : 300000; - log("VulCheckHelperTool: packet_id=" + packetId + ", vulcheck_type=" + vulCheckType - + ", locations=" + targetLocationsJson.size() + ", mode=" + mode + ", max_payloads=" + maxPayloads); + log("VulCheckHelperTool: packet_id=" + packetId + ", vulcheck_type=" + vulCheckType + ", locations=" + + targetLocationsJson.size() + ", mode=" + mode + ", max_payloads=" + maxPayloads); // パケットを取得 Packet originalPacket = Packets.getInstance().query(packetId); @@ -179,17 +185,18 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception // VulCheckerを取得 VulChecker vulChecker = VulCheckerManager.getInstance().createInstance(vulCheckType); if (vulChecker == null) { - throw new IllegalArgumentException("VulCheck type '" + vulCheckType + "' not found. Use 'list' to see available types."); + throw new IllegalArgumentException( + "VulCheck type '" + vulCheckType + "' not found. Use 'list' to see available types."); } // ターゲット位置を解析 List targetLocations = parseTargetLocations(targetLocationsJson, originalOneShot.getData()); - + long startTime = System.currentTimeMillis(); - + // VulCheckテストを実行 - VulCheckResult result = executeVulCheckTests(originalOneShot, vulChecker, targetLocations, - intervalMs, mode, maxPayloads, timeoutMs); + VulCheckResult result = executeVulCheckTests(originalOneShot, vulChecker, targetLocations, intervalMs, mode, + maxPayloads, timeoutMs); long executionTime = System.currentTimeMillis() - startTime; @@ -231,12 +238,15 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception JsonObject performance = new JsonObject(); performance.addProperty("average_interval_ms", result.averageIntervalMs); performance.addProperty("payloads_per_second", (double) result.totalPacketsSent / (executionTime / 1000.0)); - performance.addProperty("success_rate_percent", result.totalPacketsSent > 0 - ? ((double)(result.totalPacketsSent - result.totalFailed) / result.totalPacketsSent) * 100.0 : 0.0); + performance.addProperty("success_rate_percent", + result.totalPacketsSent > 0 + ? ((double) (result.totalPacketsSent - result.totalFailed) / result.totalPacketsSent) * 100.0 + : 0.0); response.add("performance", performance); - log("VulCheckHelperTool: Completed. Generated " + result.totalPayloadsGenerated + " payloads, sent " - + result.totalPacketsSent + " packets, " + result.totalFailed + " failed, time: " + executionTime + "ms"); + log("VulCheckHelperTool: Completed. Generated " + result.totalPayloadsGenerated + " payloads, sent " + + result.totalPacketsSent + " packets, " + result.totalFailed + " failed, time: " + executionTime + + "ms"); return response; } @@ -250,7 +260,7 @@ private JsonObject getAvailableVulCheckTypes() throws Exception { JsonObject result = new JsonObject(); result.addProperty("available_vulcheck_types", String.join(", ", vulCheckerNames)); - + JsonArray typesArray = new JsonArray(); Map typeDetails = new HashMap<>(); @@ -260,7 +270,7 @@ private JsonObject getAvailableVulCheckTypes() throws Exception { JsonObject typeInfo = new JsonObject(); typeInfo.addProperty("name", name); typeInfo.addProperty("description", "VulCheck tests for " + name + " vulnerabilities"); - + // ジェネレータの詳細を追加 ImmutableList generators = checker.getGenerators(); JsonArray generatorsList = new JsonArray(); @@ -272,15 +282,15 @@ private JsonObject getAvailableVulCheckTypes() throws Exception { } typeInfo.add("generators", generatorsList); typeInfo.addProperty("generator_count", generators.size()); - + typesArray.add(typeInfo); typeDetails.put(name, typeInfo); } } - + result.add("vulcheck_types", typesArray); result.addProperty("total_types", vulCheckerNames.length); - + return result; } @@ -290,37 +300,39 @@ private JsonObject getAvailableVulCheckTypes() throws Exception { private List parseTargetLocations(JsonArray targetLocations, byte[] packetData) throws Exception { List locations = new ArrayList<>(); String packetStr = new String(packetData); - + for (JsonElement element : targetLocations) { JsonObject location = element.getAsJsonObject(); - + // Check if regex pattern is specified if (location.has("pattern")) { // Regex-based approach String pattern = location.get("pattern").getAsString(); String replacement = location.has("replacement") ? location.get("replacement").getAsString() : null; - String description = location.has("description") ? location.get("description").getAsString() : ("Pattern: " + pattern); - + String description = location.has("description") + ? location.get("description").getAsString() + : ("Pattern: " + pattern); + // Find all matches for the pattern Pattern regex = Pattern.compile(pattern); Matcher matcher = regex.matcher(packetStr); - + int matchCount = 0; while (matcher.find()) { matchCount++; int start = matcher.start(); int end = matcher.end(); - + TargetLocation targetLoc = new TargetLocation(); targetLoc.range = Range.of(start, end); targetLoc.description = description + " (match " + matchCount + ")"; targetLoc.pattern = pattern; targetLoc.replacement = replacement; targetLoc.originalMatch = matcher.group(); - + locations.add(targetLoc); } - + if (matchCount == 0) { log("VulCheckHelperTool: No matches found for pattern: " + pattern); } @@ -328,113 +340,120 @@ private List parseTargetLocations(JsonArray targetLocations, byt // Position-based approach (backward compatibility) int start = location.get("start").getAsInt(); int end = location.get("end").getAsInt(); - String description = location.has("description") ? location.get("description").getAsString() : ("Range " + start + "-" + end); - + String description = location.has("description") + ? location.get("description").getAsString() + : ("Range " + start + "-" + end); + if (start < 0 || end < start || end > packetData.length) { - throw new IllegalArgumentException("Invalid range: start=" + start + ", end=" + end + ", packet length=" + packetData.length); + throw new IllegalArgumentException( + "Invalid range: start=" + start + ", end=" + end + ", packet length=" + packetData.length); } - + TargetLocation targetLoc = new TargetLocation(); targetLoc.range = Range.of(start, end); targetLoc.description = description; targetLoc.originalMatch = packetStr.substring(start, end); - + locations.add(targetLoc); } else { - throw new IllegalArgumentException("Each target location must have either 'pattern' or both 'start' and 'end' properties"); + throw new IllegalArgumentException( + "Each target location must have either 'pattern' or both 'start' and 'end' properties"); } } - + if (locations.isEmpty()) { throw new IllegalArgumentException("No valid target locations found"); } - + return locations; } /** * VulCheckテストを実行 */ - private VulCheckResult executeVulCheckTests(OneShotPacket originalPacket, VulChecker vulChecker, - List targetLocations, int intervalMs, String mode, int maxPayloads, int timeoutMs) throws Exception { - + private VulCheckResult executeVulCheckTests(OneShotPacket originalPacket, VulChecker vulChecker, + List targetLocations, int intervalMs, String mode, int maxPayloads, int timeoutMs) + throws Exception { + VulCheckResult result = new VulCheckResult(); result.overallSuccess = true; - + // 各ターゲット位置に対してテストを実行 for (int locationIndex = 0; locationIndex < targetLocations.size(); locationIndex++) { TargetLocation targetLocation = targetLocations.get(locationIndex); - - log("VulCheckHelperTool: Processing target location " + (locationIndex + 1) + "/" + targetLocations.size() - + " at range " + targetLocation.range.getPositionStart() + "-" + targetLocation.range.getPositionEnd() - + " (" + targetLocation.description + ")"); - - LocationResult locResult = processTargetLocation(originalPacket, vulChecker, targetLocation, - intervalMs, mode, maxPayloads); - + + log("VulCheckHelperTool: Processing target location " + (locationIndex + 1) + "/" + targetLocations.size() + + " at range " + targetLocation.range.getPositionStart() + "-" + + targetLocation.range.getPositionEnd() + " (" + targetLocation.description + ")"); + + LocationResult locResult = processTargetLocation(originalPacket, vulChecker, targetLocation, intervalMs, + mode, maxPayloads); + result.locationResults.add(locResult); result.totalPayloadsGenerated += locResult.payloadsGenerated; result.totalPacketsSent += locResult.packetsSent; result.totalFailed += locResult.packetsFailed; - + if (locResult.packetsFailed > 0) { result.overallSuccess = false; } } - + // 平均間隔を計算 if (result.totalPacketsSent > 1) { result.averageIntervalMs = intervalMs; // sequentialモードの場合 } - + return result; } /** * 特定のターゲット位置でVulCheckテストを実行 */ - private LocationResult processTargetLocation(OneShotPacket originalPacket, VulChecker vulChecker, + private LocationResult processTargetLocation(OneShotPacket originalPacket, VulChecker vulChecker, TargetLocation targetLocation, int intervalMs, String mode, int maxPayloads) throws Exception { - + LocationResult result = new LocationResult(); result.range = targetLocation.range; result.description = targetLocation.description; - + long startTime = System.currentTimeMillis(); - + // 元のパケットデータを取得 byte[] originalData = originalPacket.getData(); String originalText = new String(originalData); - + // ターゲット範囲のデータを取得 if (targetLocation.range.getPositionEnd() > originalData.length) { - throw new IllegalArgumentException("Target range exceeds packet data length: range end=" - + targetLocation.range.getPositionEnd() + ", data length=" + originalData.length); + throw new IllegalArgumentException("Target range exceeds packet data length: range end=" + + targetLocation.range.getPositionEnd() + ", data length=" + originalData.length); } - - String targetData = targetLocation.originalMatch != null ? targetLocation.originalMatch - : originalText.substring(targetLocation.range.getPositionStart(), targetLocation.range.getPositionEnd()); - + + String targetData = targetLocation.originalMatch != null + ? targetLocation.originalMatch + : originalText.substring(targetLocation.range.getPositionStart(), + targetLocation.range.getPositionEnd()); + // VulCheckのジェネレータを取得してペイロードを生成 ImmutableList generators = vulChecker.getGenerators(); ResendController resendController = ResendController.getInstance(); - + int payloadCount = 0; int sentCount = 0; int failedCount = 0; - + for (Generator generator : generators) { if (payloadCount >= maxPayloads) { log("VulCheckHelperTool: Reached max payload limit (" + maxPayloads + ")"); break; } - + try { String payload = generator.generate(targetData); if (payload != null && !payload.equals(targetData)) { result.generatedPayloads.add(payload); payloadCount++; - + // パケットを改変して送信 String modifiedText; if (targetLocation.pattern != null && targetLocation.replacement != null) { @@ -442,7 +461,7 @@ private LocationResult processTargetLocation(OneShotPacket originalPacket, VulCh String replacementTemplate = targetLocation.replacement.replace("$1", payload); Pattern pattern = Pattern.compile(targetLocation.pattern); Matcher matcher = pattern.matcher(originalText); - + // Find the specific match we're targeting StringBuffer sb = new StringBuffer(); int currentMatch = 0; @@ -460,19 +479,18 @@ private LocationResult processTargetLocation(OneShotPacket originalPacket, VulCh modifiedText = sb.toString(); } else { // Position-based replacement approach - modifiedText = originalText.substring(0, targetLocation.range.getPositionStart()) - + payload - + originalText.substring(targetLocation.range.getPositionEnd()); + modifiedText = originalText.substring(0, targetLocation.range.getPositionStart()) + payload + + originalText.substring(targetLocation.range.getPositionEnd()); } - + byte[] modifiedData = modifiedText.getBytes(); - - OneShotPacket modifiedPacket = new OneShotPacket(originalPacket.getId(), - originalPacket.getListenPort(), originalPacket.getClient(), originalPacket.getServer(), - originalPacket.getServerName(), originalPacket.getUseSSL(), modifiedData, - originalPacket.getEncoder(), originalPacket.getAlpn(), originalPacket.getDirection(), - originalPacket.getConn(), originalPacket.getGroup()); - + + OneShotPacket modifiedPacket = new OneShotPacket(originalPacket.getId(), + originalPacket.getListenPort(), originalPacket.getClient(), originalPacket.getServer(), + originalPacket.getServerName(), originalPacket.getUseSSL(), modifiedData, + originalPacket.getEncoder(), originalPacket.getAlpn(), originalPacket.getDirection(), + originalPacket.getConn(), originalPacket.getGroup()); + // 送信モードに応じて処理 if ("parallel".equals(mode)) { // 並列送信 - すぐに送信 @@ -488,7 +506,7 @@ private LocationResult processTargetLocation(OneShotPacket originalPacket, VulCh try { resendController.resend(modifiedPacket); sentCount++; - + if (intervalMs > 0 && payloadCount < maxPayloads && payloadCount < generators.size()) { Thread.sleep(intervalMs); } @@ -499,19 +517,20 @@ private LocationResult processTargetLocation(OneShotPacket originalPacket, VulCh } } } catch (Exception e) { - log("VulCheckHelperTool: Failed to generate payload with generator " + generator.getName() + ": " + e.getMessage()); + log("VulCheckHelperTool: Failed to generate payload with generator " + generator.getName() + ": " + + e.getMessage()); failedCount++; } } - + result.payloadsGenerated = payloadCount; result.packetsSent = sentCount; result.packetsFailed = failedCount; result.executionTimeMs = System.currentTimeMillis() - startTime; - - log("VulCheckHelperTool: Location complete - generated " + payloadCount + " payloads, sent " - + sentCount + " packets, " + failedCount + " failed"); - + + log("VulCheckHelperTool: Location complete - generated " + payloadCount + " payloads, sent " + sentCount + + " packets, " + failedCount + " failed"); + return result; } @@ -533,9 +552,9 @@ private static class VulCheckResult { private static class TargetLocation { Range range; String description; - String pattern; // regex pattern (if used) - String replacement; // replacement template (if used) - String originalMatch; // original matched text + String pattern; // regex pattern (if used) + String replacement; // replacement template (if used) + String originalMatch; // original matched text } /** @@ -550,4 +569,4 @@ private static class LocationResult { long executionTimeMs = 0; List generatedPayloads = new ArrayList<>(); } -} \ No newline at end of file +} From 65e5459f03145f8f1dfdebc050b83283b1cf2635 Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 11 Aug 2025 01:40:13 +0900 Subject: [PATCH 31/39] =?UTF-8?q?access=5Ftoken=20=E3=81=8C=E4=B8=8D?= =?UTF-8?q?=E6=84=8F=E3=81=AB=E3=83=AD=E3=82=B0=E3=81=95=E3=82=8C=E3=81=AA?= =?UTF-8?q?=E3=81=84=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/mcp/MCPServerExtension.java | 41 ++++++++++++++++++- .../mcp/tools/AuthenticatedMCPTool.java | 13 +++++- .../extensions/mcp/tools/BulkSendTool.java | 1 + .../extensions/mcp/tools/ConfigTool.java | 2 +- .../extensions/mcp/tools/HistoryTool.java | 2 +- .../extensions/mcp/tools/LogTool.java | 2 +- .../mcp/tools/PacketDetailTool.java | 2 +- .../mcp/tools/ResendPacketTool.java | 1 + .../mcp/tools/RestoreConfigTool.java | 2 +- .../mcp/tools/UpdateConfigTool.java | 2 +- .../mcp/tools/VulCheckHelperTool.java | 1 + 11 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java index 1ab0cb68..0ca82f47 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java @@ -15,6 +15,7 @@ import java.nio.charset.StandardCharsets; import javax.swing.BoxLayout; import javax.swing.JButton; +import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenuItem; @@ -30,8 +31,10 @@ public class MCPServerExtension extends Extension { private JTextArea logArea; private JButton startButton; private JButton stopButton; + private JCheckBox maskTokenCheckBox; private boolean isRunning = false; private static final int HTTP_PORT = 8765; + private java.util.List logMessages = new java.util.ArrayList<>(); public MCPServerExtension() { super(); @@ -57,6 +60,16 @@ public JComponent createPanel() throws Exception { stopButton = new JButton("Stop Server"); stopButton.setEnabled(false); + maskTokenCheckBox = new JCheckBox("Mask access_token in logs", true); + maskTokenCheckBox.setToolTipText("When enabled, access_token values are masked with asterisks in log display"); + maskTokenCheckBox.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 15, 0, 0)); + maskTokenCheckBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + refreshLogDisplay(); + } + }); + startButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -73,6 +86,7 @@ public void actionPerformed(ActionEvent e) { statusPanel.add(startButton); statusPanel.add(stopButton); + statusPanel.add(maskTokenCheckBox); // Log area logArea = new JTextArea(20, 80); @@ -158,14 +172,39 @@ private void stopServer() { } private void addLog(String message) { + synchronized (logMessages) { + logMessages.add(message); + } if (logArea != null) { javax.swing.SwingUtilities.invokeLater(() -> { - logArea.append("[" + new java.util.Date() + "] " + message + "\n"); + String displayMessage = maskTokenCheckBox.isSelected() ? maskAccessToken(message) : message; + logArea.append("[" + new java.util.Date() + "] " + displayMessage + "\n"); logArea.setCaretPosition(logArea.getDocument().getLength()); }); } } + private void refreshLogDisplay() { + if (logArea != null) { + javax.swing.SwingUtilities.invokeLater(() -> { + logArea.setText(""); + synchronized (logMessages) { + for (String message : logMessages) { + String displayMessage = maskTokenCheckBox.isSelected() ? maskAccessToken(message) : message; + logArea.append("[" + new java.util.Date() + "] " + displayMessage + "\n"); + } + } + logArea.setCaretPosition(logArea.getDocument().getLength()); + }); + } + } + + private String maskAccessToken(String message) { + // Pattern to match "access_token":"value" or "access_token": "value" or + // access_token=value + return message.replaceAll("(?i)(\"?access_token\"?\\s*[:=]\\s*\"?)([^\"\\s&,}]+)(\"?)", "$1****$3"); + } + // HTTP handler for MCP requests private class MCPHttpHandler implements HttpHandler { @Override diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java index 20abe97e..631fed33 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java @@ -41,7 +41,7 @@ protected void validateAccessToken(JsonObject arguments) throws Exception { // トークンの照合 if (!configuredToken.equals(providedToken)) { - log("Access token validation failed. Provided: " + providedToken + ", Expected: " + configuredToken); + log("Access token validation failed"); throw new Exception( "Invalid access token. Please check your access token from PacketProxy Settings > Import/Export configs section."); } @@ -72,6 +72,17 @@ protected JsonObject addAccessTokenToSchema(JsonObject schema) { return schema; } + /** + * access_tokenをマスクした安全なargumentsの文字列表現を返す + */ + protected String getSafeArgumentsString(JsonObject arguments) { + JsonObject safeArgs = arguments.deepCopy(); + if (safeArgs.has("access_token")) { + safeArgs.addProperty("access_token", "****"); + } + return safeArgs.toString(); + } + /** * サブクラスで実装する認証後の実際の処理 */ diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java index 223eaae1..710c1fc5 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java @@ -201,6 +201,7 @@ public JsonObject getInputSchema() { @Override protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("BulkSendTool called with arguments: " + getSafeArgumentsString(arguments)); log("BulkSendTool: Starting bulk send operation"); // パラメータ取得 diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java index adc6fb2f..f91d160e 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java @@ -57,7 +57,7 @@ public JsonObject getInputSchema() { @Override protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { - log("ConfigTool called with arguments: " + arguments.toString()); + log("ConfigTool called with arguments: " + getSafeArgumentsString(arguments)); try { // HTTP APIで設定を取得 diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java index 12452a00..a8a2f540 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java @@ -66,7 +66,7 @@ public JsonObject getInputSchema() { @Override protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { - log("HistoryTool called with arguments: " + arguments.toString()); + log("HistoryTool called with arguments: " + getSafeArgumentsString(arguments)); int limit = arguments.has("limit") ? arguments.get("limit").getAsInt() : 100; int offset = arguments.has("offset") ? arguments.get("offset").getAsInt() : 0; diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java index f2bba518..fa2c1ef6 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java @@ -62,7 +62,7 @@ public JsonObject getInputSchema() { @Override protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { - log("LogTool called with arguments: " + arguments.toString()); + log("LogTool called with arguments: " + getSafeArgumentsString(arguments)); String level = arguments.has("level") ? arguments.get("level").getAsString() : "info"; int limit = arguments.has("limit") ? arguments.get("limit").getAsInt() : 100; diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java index 1cbb15ed..a7277955 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java @@ -53,7 +53,7 @@ public JsonObject getInputSchema() { @Override protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { - log("PacketDetailTool called with arguments: " + arguments.toString()); + log("PacketDetailTool called with arguments: " + getSafeArgumentsString(arguments)); if (!arguments.has("packet_id")) { throw new Exception("packet_id is required"); diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java index e264e26a..c1329ea5 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java @@ -125,6 +125,7 @@ public JsonObject getInputSchema() { @Override protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("ResendPacketTool called with arguments: " + getSafeArgumentsString(arguments)); log("ResendPacketTool: Starting packet resend operation"); // パラメータ取得 diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java index 56d2e739..f10aaf40 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java @@ -44,7 +44,7 @@ public JsonObject getInputSchema() { @Override protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { - log("RestoreConfigTool called with arguments: " + arguments.toString()); + log("RestoreConfigTool called with arguments: " + getSafeArgumentsString(arguments)); if (!arguments.has("backup_id")) { throw new Exception("backup_id parameter is required"); diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java index bec2565a..0fbb9f50 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java @@ -59,7 +59,7 @@ public JsonObject getInputSchema() { @Override protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { - log("UpdateConfigTool called with arguments: " + arguments.toString()); + log("UpdateConfigTool called with arguments: " + getSafeArgumentsString(arguments)); if (!arguments.has("config_json")) { throw new Exception("config_json parameter is required"); diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java index aef1b18d..4749ccdb 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java @@ -130,6 +130,7 @@ public JsonObject getInputSchema() { @Override protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("VulCheckHelperTool called with arguments: " + getSafeArgumentsString(arguments)); log("VulCheckHelperTool: Starting VulCheck operation"); // パラメータ取得 From 685a1c082ff325e13ff2865de02d9e7dc5d9564b Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 11 Aug 2025 01:50:32 +0900 Subject: [PATCH 32/39] =?UTF-8?q?include=5Fpair=E3=81=AE=E3=83=87=E3=83=95?= =?UTF-8?q?=E3=82=A9=E3=83=AB=E3=83=88=E5=80=A4=E3=82=92false=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-setting-guide.md | 3 ++- doc/mcp-server-spec.md | 6 +++--- .../extensions/mcp/tools/PacketDetailTool.java | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/doc/mcp-server-setting-guide.md b/doc/mcp-server-setting-guide.md index af6edcb7..bdc8b6d6 100644 --- a/doc/mcp-server-setting-guide.md +++ b/doc/mcp-server-setting-guide.md @@ -235,7 +235,8 @@ PacketProxyの現在の設定を確認してください **パラメータ:** - `packet_id` (integer, required): パケットID -- `include_body` (boolean, optional): ボディを含めるか +- `include_body` (boolean, optional): ボディを含めるか (デフォルト: false) +- `include_pair` (boolean, optional): ペアパケットを含めるか (デフォルト: false) **使用例:** diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index c6e7a2f9..7504d2a0 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -134,7 +134,7 @@ PacketProxyのパケット履歴を検索・取得します。フィルタリン "access_token": "your_access_token_here", "packet_id": 123, "include_body": true, - "include_pair": true + "include_pair": false } }, "id": 2 @@ -144,8 +144,8 @@ PacketProxyのパケット履歴を検索・取得します。フィルタリン **パラメータ:** - `access_token` (string, required): PacketProxy設定のアクセストークン - `packet_id` (number, required): パケットID(リクエストまたはレスポンスのどちらでも指定可能) -- `include_body` (boolean, optional): リクエスト/レスポンスボディを含める (デフォルト: false) -- `include_pair` (boolean, optional): ペアパケット(リクエスト指定時はレスポンス、レスポンス指定時はリクエスト)を含める (デフォルト: true) +- `include_body` (boolean, optional): リクエスト/レスポンスボディを含める (デフォルト: true) +- `include_pair` (boolean, optional): ペアパケット(リクエスト指定時はレスポンス、レスポンス指定時はリクエスト)を含める (デフォルト: false) **レスポンス(ペアが見つかった場合):** diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java index a7277955..128f6af0 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java @@ -38,14 +38,14 @@ public JsonObject getInputSchema() { JsonObject includeBodyProp = new JsonObject(); includeBodyProp.addProperty("type", "boolean"); includeBodyProp.addProperty("description", "Whether to include request/response body"); - includeBodyProp.addProperty("default", false); + includeBodyProp.addProperty("default", true); schema.add("include_body", includeBodyProp); JsonObject includePairProp = new JsonObject(); includePairProp.addProperty("type", "boolean"); includePairProp.addProperty("description", "Whether to include paired packet (request when response specified, response when request specified)"); - includePairProp.addProperty("default", true); + includePairProp.addProperty("default", false); schema.add("include_pair", includePairProp); return addAccessTokenToSchema(schema); @@ -60,8 +60,8 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception } int packetId = arguments.get("packet_id").getAsInt(); - boolean includeBody = arguments.has("include_body") && arguments.get("include_body").getAsBoolean(); - boolean includePair = !arguments.has("include_pair") || arguments.get("include_pair").getAsBoolean(); + boolean includeBody = !arguments.has("include_body") || arguments.get("include_body").getAsBoolean(); + boolean includePair = arguments.has("include_pair") && arguments.get("include_pair").getAsBoolean(); try { Packets packets = Packets.getInstance(); From 61b01004d5ebd6c3978803f4ee9b464b3beafffb Mon Sep 17 00:00:00 2001 From: kakira Date: Mon, 11 Aug 2025 02:11:24 +0900 Subject: [PATCH 33/39] =?UTF-8?q?%0a=20=E3=81=AA=E3=81=A9=E3=82=92format?= =?UTF-8?q?=E3=81=AB=E4=B8=8E=E3=81=88=E3=81=9F=E3=81=A8=E3=81=8D=E3=81=AE?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/core/packetproxy/util/Logging.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/core/packetproxy/util/Logging.java b/src/main/java/core/packetproxy/util/Logging.java index fc843d2c..edc0b9cf 100644 --- a/src/main/java/core/packetproxy/util/Logging.java +++ b/src/main/java/core/packetproxy/util/Logging.java @@ -30,7 +30,14 @@ private Logging() { public static void log(String format, Object... args) { LocalDateTime now = LocalDateTime.now(); String ns = dtf.format(now); - String ss = ns + " " + String.format(format, args); + String ss; + if (args.length == 0) { + // 引数がない場合は、formatをそのまま使用(String.formatを使わない) + ss = ns + " " + format; + } else { + // 引数がある場合のみString.formatを使用 + ss = ns + " " + String.format(format, args); + } System.out.println(ss); guiLog.append(ss); } @@ -38,7 +45,14 @@ public static void log(String format, Object... args) { public static void err(String format, Object... args) { LocalDateTime now = LocalDateTime.now(); String ns = dtf.format(now); - String ss = ns + " " + String.format(format, args); + String ss; + if (args.length == 0) { + // 引数がない場合は、formatをそのまま使用(String.formatを使わない) + ss = ns + " " + format; + } else { + // 引数がある場合のみString.formatを使用 + ss = ns + " " + String.format(format, args); + } System.err.println(ss); guiLog.appendErr(ss); } From 71bdb04f0b50fdd4f41e4f84b70efa9204aa95bf Mon Sep 17 00:00:00 2001 From: kakira Date: Tue, 12 Aug 2025 15:36:47 +0900 Subject: [PATCH 34/39] =?UTF-8?q?content=20=E3=81=AB=E5=8C=85=E3=82=80?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/mcp/tools/ToolRegistry.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java index 8326b025..850edabb 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java @@ -56,6 +56,24 @@ public JsonObject callTool(String toolName, JsonObject arguments) throws Excepti throw new Exception("Unknown tool: " + toolName); } - return tool.call(arguments); + JsonObject toolResult = tool.call(arguments); + + // MCP仕様に準拠した応答形式に変換 + JsonObject mcpResponse = new JsonObject(); + + // content配列を作成 (必須) + JsonArray content = new JsonArray(); + JsonObject textContent = new JsonObject(); + textContent.addProperty("type", "text"); + textContent.addProperty("text", toolResult.toString()); + content.add(textContent); + + mcpResponse.add("content", content); + mcpResponse.addProperty("isError", false); + + // 元の結果をstructuredContentとして保持(オプション) + mcpResponse.add("structuredContent", toolResult); + + return mcpResponse; } } From e556d130a12d8b0f780ab689a58bf342e938e5c3 Mon Sep 17 00:00:00 2001 From: kakira Date: Tue, 12 Aug 2025 15:42:36 +0900 Subject: [PATCH 35/39] =?UTF-8?q?=E3=82=AB=E3=83=A9=E3=83=A0=E5=90=8D?= =?UTF-8?q?=E5=91=A8=E3=82=8A=E3=81=AE=E8=AA=AC=E6=98=8E=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/packetproxy/extensions/mcp/tools/HistoryTool.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java index a8a2f540..eb21cda0 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java @@ -48,9 +48,10 @@ public JsonObject getInputSchema() { JsonObject filterProp = new JsonObject(); filterProp.addProperty("type", "string"); filterProp.addProperty("description", "PacketProxy Filter syntax for filtering packets. " - + "Available columns: id, request, response, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, alpn, group, full_text, full_text_i, method, url, status. " + + "Available columns: id, request, response, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, alpn, group, full_text, full_text_i. " + + "Note: method, url, status are NOT available for filtering (only for ordering). " + "Operators: == (equals), != (not equals), >= (greater or equal), <= (less or equal), =~ (regex match), !~ (regex not match), && (AND), || (OR). " - + "Examples: 'method == GET', 'status >= 400', 'url =~ /api/', 'method == POST && status >= 400', 'length > 1000', 'full_text_i =~ authorization'"); + + "Examples: 'type == HTTP', 'length > 1000', 'full_text_i =~ authorization', 'client_port == 80 && server_port == 443'"); schema.add("filter", filterProp); JsonObject orderProp = new JsonObject(); From b8f5b7ee47a3bbfcd1ed033831833ec05f5d6561 Mon Sep 17 00:00:00 2001 From: kakira Date: Wed, 13 Aug 2025 04:08:28 +0900 Subject: [PATCH 36/39] =?UTF-8?q?=E5=B8=B0=E3=82=8A=E5=80=A4=E3=81=AB?= =?UTF-8?q?=E3=83=91=E3=82=B1=E3=83=83=E3=83=88ID=E3=82=92=E5=90=AB?= =?UTF-8?q?=E3=82=81=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../packetproxy/extensions/mcp/tools/BulkSendTool.java | 10 ---------- .../extensions/mcp/tools/ResendPacketTool.java | 7 ------- 2 files changed, 17 deletions(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java index 710c1fc5..34c9572d 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java @@ -319,12 +319,6 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception resultObj.addProperty("sent_count", r.sentCount); resultObj.addProperty("failed_count", r.failedCount); - JsonArray newPacketIds = new JsonArray(); - for (Integer id : r.newPacketIds) { - newPacketIds.add(id); - } - resultObj.add("new_packet_ids", newPacketIds); - if (r.error != null) { resultObj.addProperty("error", r.error); } @@ -374,7 +368,6 @@ private BulkSendResult processSinglePacket(int packetId, int packetIndex, int co BulkSendResult result = new BulkSendResult(); result.originalPacketId = packetId; result.packetIndex = packetIndex; - result.newPacketIds = new ArrayList<>(); result.regexParamsApplied = new ArrayList<>(); long startTime = System.currentTimeMillis(); @@ -455,7 +448,6 @@ protected void done() { } else { result.success = true; result.sentCount = count; - // 新しいパケットIDは実際の実装では取得困難なため空のままとする } } catch (Exception e) { @@ -480,7 +472,6 @@ private BulkSendResult processSinglePacketSequential(int packetId, int packetInd BulkSendResult result = new BulkSendResult(); result.originalPacketId = packetId; result.packetIndex = packetIndex; - result.newPacketIds = new ArrayList<>(); result.regexParamsApplied = new ArrayList<>(); long startTime = System.currentTimeMillis(); @@ -912,7 +903,6 @@ private static class BulkSendResult { boolean success; int sentCount; int failedCount; - List newPacketIds; String error; long executionTimeMs; List regexParamsApplied; diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java index c1329ea5..58991c41 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java @@ -175,7 +175,6 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception long startTime = System.currentTimeMillis(); int sentCount = 0; int failedCount = 0; - List newPacketIds = new ArrayList<>(); try { ResendController resendController = ResendController.getInstance(); @@ -222,12 +221,6 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception result.addProperty("failed_count", failedCount); result.addProperty("execution_time_ms", executionTime); - JsonArray packetIdsArray = new JsonArray(); - for (Integer id : newPacketIds) { - packetIdsArray.add(id); - } - result.add("packet_ids", packetIdsArray); - log("ResendPacketTool: Completed. Sent: " + sentCount + ", Failed: " + failedCount + ", Time: " + executionTime + "ms"); return result; From 4a2918cc62ea4a2e947c4641ec531b982eb2a20e Mon Sep 17 00:00:00 2001 From: kakira Date: Wed, 13 Aug 2025 04:46:03 +0900 Subject: [PATCH 37/39] =?UTF-8?q?=E9=80=81=E4=BF=A1=E7=B3=BB=E3=83=84?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E4=BD=BF=E7=94=A8=E6=99=82=E3=81=ABID?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E8=B7=A1=E3=81=99=E3=82=8B=E4=BB=95=E7=B5=84?= =?UTF-8?q?=E3=81=BF=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-spec.md | 105 ++++++- .../java/core/packetproxy/DuplexFactory.java | 12 + .../extensions/mcp/tools/BulkSendTool.java | 34 ++- .../extensions/mcp/tools/JobStatusTool.java | 262 ++++++++++++++++++ .../mcp/tools/ResendPacketTool.java | 30 +- .../extensions/mcp/tools/ToolRegistry.java | 11 +- .../mcp/tools/VulCheckHelperTool.java | 20 +- .../core/packetproxy/model/OneShotPacket.java | 34 ++- .../java/core/packetproxy/model/Packet.java | 31 ++- .../java/core/packetproxy/model/Packets.java | 9 +- 10 files changed, 511 insertions(+), 37 deletions(-) create mode 100644 src/main/java/core/packetproxy/extensions/mcp/tools/JobStatusTool.java diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index 7504d2a0..36d8ec26 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -584,7 +584,7 @@ IMPORTANT: Requires a complete configuration object, not partial updates. "success": true, "sent_count": 20, "failed_count": 0, - "packet_ids": [124, 125, 126], + "job_id": "af2adff0-a35a-43ef-b653-cb47203727df", "execution_time_ms": 2100 }, "id": 7 @@ -702,7 +702,7 @@ IMPORTANT: Requires a complete configuration object, not partial updates. "average_response_time_ms": 312, "concurrent_connections": 3 }, - "job_id": null + "job_id": "bulk_send_20250804_120030_abc123" }, "id": 8 } @@ -886,7 +886,8 @@ IMPORTANT: Requires a complete configuration object, not partial updates. "average_interval_ms": 100, "payloads_per_second": 6.67, "success_rate_percent": 88.89 - } + }, + "job_id": "vulcheck_20250804_120030_def456" }, "id": 9 } @@ -926,6 +927,103 @@ curl -X POST http://localhost:32349/mcp/tools/call \ }' ``` +### 10. `get_job_status` - ジョブ状況取得 + +send系ツール(resend_packet/bulk_send/call_vulcheck_helper)で作成されたジョブの実行状況を取得します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_job_status", + "arguments": { + "job_id": "af2adff0-a35a-43ef-b653-cb47203727df" + } + }, + "id": 10 +} +``` + +**パラメータ:** +- `job_id` (string, optional): 取得するジョブのID。指定しない場合は全ジョブの概要を返す + +**特定ジョブの詳細レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "job_id": "af2adff0-a35a-43ef-b653-cb47203727df", + "total_requests": 5, + "requests_sent": 5, + "responses_received": 3, + "status": "receiving_responses", + "requests": [ + { + "temporary_id": "temp_001", + "has_request": true, + "has_response": true, + "request_packet_id": 124, + "response_packet_id": 125 + }, + { + "temporary_id": "temp_002", + "has_request": true, + "has_response": false, + "request_packet_id": 126 + } + ] + }, + "id": 10 +} +``` + +**全ジョブ概要レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "total_jobs": 3, + "jobs": [ + { + "job_id": "af2adff0-a35a-43ef-b653-cb47203727df", + "total_requests": 5, + "requests_sent": 5, + "responses_received": 3, + "status": "receiving_responses" + }, + { + "job_id": "bulk_send_20250804_120030_abc123", + "total_requests": 10, + "requests_sent": 10, + "responses_received": 10, + "status": "completed" + } + ] + }, + "id": 10 +} +``` + +**ジョブ状態:** +- `created`: ジョブは作成されたがリクエストはまだ送信されていない +- `requests_sent`: 全リクエストが送信済み、レスポンス待ち +- `receiving_responses`: 一部のレスポンスを受信中 +- `completed`: 全リクエスト・レスポンスが完了 + +**ジョブの概念:** + +PacketProxyのジョブシステムは、send系ツールで送信されたパケットの追跡を可能にします: + +- **job_id**: 各send系ツール実行時に生成されるUUID +- **temporary_id**: ジョブ内の各リクエスト/レスポンスペアを識別するUUID +- **パケット関連付け**: 送信されたパケットと受信されたレスポンスがtemporary_idで関連付けられる +- **状況追跡**: データベースに保存されたパケット履歴からジョブの進行状況をリアルタイムで取得 + ## フィルタ構文仕様 PacketProxyのFilterTextParserに準拠した構文を使用します。 @@ -1008,6 +1106,7 @@ PUT /mcp/configs # 設定更新 POST /mcp/resend/{packet_id} # パケット再送 POST /mcp/bulk_send # 複数パケット一括送信 POST /mcp/call_vulcheck_helper # VulCheck脆弱性テストヘルパー +GET /mcp/job_status?job_id=... # ジョブ状況取得 GET /mcp/logs?level=info # ログ取得 POST /mcp/restore/{backup_id} # バックアップ復元 ``` diff --git a/src/main/java/core/packetproxy/DuplexFactory.java b/src/main/java/core/packetproxy/DuplexFactory.java index f1d7e5a3..059f15e1 100644 --- a/src/main/java/core/packetproxy/DuplexFactory.java +++ b/src/main/java/core/packetproxy/DuplexFactory.java @@ -386,6 +386,9 @@ public byte[] onServerChunkReceived(byte[] data) throws Exception { server_packet = new Packet(0, oneshot.getClient(), oneshot.getServer(), oneshot.getServerName(), oneshot.getUseSSL(), oneshot.getEncoder(), oneshot.getAlpn(), Packet.Direction.SERVER, duplex.hashCode(), group_id); + // OneShotPacketからjob_idとtemporary_idを引き継ぎ + server_packet.setJobId(oneshot.getJobId()); + server_packet.setTemporaryId(oneshot.getTemporaryId()); packets.update(server_packet); server_packet.setReceivedData(data); if (data.length < SKIP_LENGTH) { @@ -416,6 +419,9 @@ public byte[] onClientChunkSend(byte[] data) throws Exception { client_packet = new Packet(0, oneshot.getClient(), oneshot.getServer(), oneshot.getServerName(), oneshot.getUseSSL(), oneshot.getEncoder(), oneshot.getAlpn(), Packet.Direction.CLIENT, duplex.hashCode(), UniqueID.getInstance().createId()); + // OneShotPacketからjob_idとtemporary_idを引き継ぎ + client_packet.setJobId(oneshot.getJobId()); + client_packet.setTemporaryId(oneshot.getTemporaryId()); client_packet.setModified(); client_packet.setDecodedData(data); client_packet.setModifiedData(data); @@ -564,6 +570,9 @@ public byte[] onServerChunkReceived(byte[] data) throws Exception { server_packet = new Packet(0, oneshot.getClient(), oneshot.getServer(), oneshot.getServerName(), oneshot.getUseSSL(), oneshot.getEncoder(), oneshot.getAlpn(), Packet.Direction.SERVER, original_duplex.hashCode(), group_id); + // OneShotPacketからjob_idとtemporary_idを引き継ぎ + server_packet.setJobId(oneshot.getJobId()); + server_packet.setTemporaryId(oneshot.getTemporaryId()); packets.update(server_packet); server_packet.setReceivedData(data); if (data.length < SKIP_LENGTH) { @@ -594,6 +603,9 @@ public byte[] onClientChunkSend(byte[] data) throws Exception { client_packet = new Packet(0, oneshot.getClient(), oneshot.getServer(), oneshot.getServerName(), oneshot.getUseSSL(), oneshot.getEncoder(), oneshot.getAlpn(), Packet.Direction.CLIENT, original_duplex.hashCode(), UniqueID.getInstance().createId()); + // OneShotPacketからjob_idとtemporary_idを引き継ぎ + client_packet.setJobId(oneshot.getJobId()); + client_packet.setTemporaryId(oneshot.getTemporaryId()); packets.update(client_packet); client_packet.setModified(); client_packet.setDecodedData(data); diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java index 34c9572d..f183bd02 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java @@ -253,6 +253,9 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception packetIds.add(element.getAsInt()); } + // ジョブIDを生成 + String jobId = UUID.randomUUID().toString(); + long startTime = System.currentTimeMillis(); int totalPackets = packetIds.size(); int totalCount = totalPackets * count; @@ -268,7 +271,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception for (int i = 0; i < packetIds.size(); i++) { int packetId = packetIds.get(i); BulkSendResult result = processSinglePacket(packetId, i, count, modifications, regexParams, - extractedValues, allowDuplicateHeaders); + extractedValues, allowDuplicateHeaders, jobId); results.add(result); sentCount += result.sentCount; failedCount += result.failedCount; @@ -279,7 +282,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception for (int i = 0; i < packetIds.size(); i++) { int packetId = packetIds.get(i); BulkSendResult result = processSinglePacketSequential(packetId, i, count, modifications, - regexParams, extractedValues, allowDuplicateHeaders, intervalMs); + regexParams, extractedValues, allowDuplicateHeaders, intervalMs, jobId); results.add(result); sentCount += result.sentCount; failedCount += result.failedCount; @@ -352,7 +355,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception performance.addProperty("concurrent_connections", totalPackets); result.add("performance", performance); - result.add("job_id", null); // 非同期実行はフェーズ3で実装 + result.addProperty("job_id", jobId); log("BulkSendTool: Completed. Sent: " + sentCount + ", Failed: " + failedCount + ", Time: " + executionTime + "ms"); @@ -363,7 +366,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception * 単一パケットの処理(並列送信) */ private BulkSendResult processSinglePacket(int packetId, int packetIndex, int count, JsonArray modifications, - JsonArray regexParams, Map extractedValues, boolean allowDuplicateHeaders) { + JsonArray regexParams, Map extractedValues, boolean allowDuplicateHeaders, String jobId) { BulkSendResult result = new BulkSendResult(); result.originalPacketId = packetId; @@ -399,10 +402,15 @@ private BulkSendResult processSinglePacket(int packetId, int packetIndex, int co OneShotPacket modifiedPacket = applyModifications(regexModifiedPacket, modifications, packetIndex + 1, allowDuplicateHeaders); - // 複数回送信用のパケット配列を作成 + // 複数回送信用のパケット配列を作成(各パケットに固有のtemporary_idを付与) OneShotPacket[] packetsToSend = new OneShotPacket[count]; for (int i = 0; i < count; i++) { - packetsToSend[i] = modifiedPacket; + String temporaryId = UUID.randomUUID().toString(); + packetsToSend[i] = new OneShotPacket(modifiedPacket.getId(), modifiedPacket.getListenPort(), + modifiedPacket.getClient(), modifiedPacket.getServer(), modifiedPacket.getServerName(), + modifiedPacket.getUseSSL(), modifiedPacket.getData(), modifiedPacket.getEncoder(), + modifiedPacket.getAlpn(), modifiedPacket.getDirection(), modifiedPacket.getConn(), + modifiedPacket.getGroup(), jobId, temporaryId); } // ResendControllerを使用して並列送信 @@ -467,7 +475,7 @@ protected void done() { */ private BulkSendResult processSinglePacketSequential(int packetId, int packetIndex, int count, JsonArray modifications, JsonArray regexParams, Map extractedValues, - boolean allowDuplicateHeaders, int intervalMs) { + boolean allowDuplicateHeaders, int intervalMs, String jobId) { BulkSendResult result = new BulkSendResult(); result.originalPacketId = packetId; @@ -509,8 +517,16 @@ private BulkSendResult processSinglePacketSequential(int packetId, int packetInd OneShotPacket modifiedPacket = applyModifications(regexModifiedPacket, modifications, packetIndex * count + i + 1, allowDuplicateHeaders); + // ジョブ情報を付与 + String temporaryId = UUID.randomUUID().toString(); + OneShotPacket jobPacket = new OneShotPacket(modifiedPacket.getId(), modifiedPacket.getListenPort(), + modifiedPacket.getClient(), modifiedPacket.getServer(), modifiedPacket.getServerName(), + modifiedPacket.getUseSSL(), modifiedPacket.getData(), modifiedPacket.getEncoder(), + modifiedPacket.getAlpn(), modifiedPacket.getDirection(), modifiedPacket.getConn(), + modifiedPacket.getGroup(), jobId, temporaryId); + // 単発送信 - ResendController.getInstance().resend(modifiedPacket); + ResendController.getInstance().resend(jobPacket); successCount++; // 同一パケット内の送信間隔 @@ -737,7 +753,7 @@ private OneShotPacket applyModifications(OneShotPacket original, JsonArray modif data = dataStr.getBytes(); - // 新しいOneShotPacketを作成 + // 新しいOneShotPacketを作成(job情報は後で付与) OneShotPacket modifiedPacket = new OneShotPacket(original.getId(), original.getListenPort(), original.getClient(), original.getServer(), original.getServerName(), original.getUseSSL(), data, original.getEncoder(), original.getAlpn(), original.getDirection(), original.getConn(), diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/JobStatusTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/JobStatusTool.java new file mode 100644 index 00000000..e9807675 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/JobStatusTool.java @@ -0,0 +1,262 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import packetproxy.model.Packet; +import packetproxy.model.Packets; + +/** + * ジョブの状況を取得するツール + */ +public class JobStatusTool extends AuthenticatedMCPTool { + + @Override + public String getName() { + return "get_job_status"; + } + + @Override + public String getDescription() { + return "Get status information for jobs created by send tools (resend_packet/bulk_send/call_vulcheck_helper). " + + "Returns job details including request/response packet counts and completion status."; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject jobIdProp = new JsonObject(); + jobIdProp.addProperty("type", "string"); + jobIdProp.addProperty("description", "Job ID to get status for. If not provided, returns status for all jobs."); + schema.add("job_id", jobIdProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("JobStatusTool called with arguments: " + getSafeArgumentsString(arguments)); + + String jobId = arguments.has("job_id") ? arguments.get("job_id").getAsString() : null; + + if (jobId != null && !jobId.trim().isEmpty()) { + // 特定のジョブの詳細を取得 + return getJobDetail(jobId); + } else { + // 全ジョブの概要を取得 + return getAllJobsStatus(); + } + } + + /** + * 特定のジョブの詳細情報を取得 + */ + private JsonObject getJobDetail(String jobId) throws Exception { + log("JobStatusTool: Getting detail for job " + jobId); + + // job_idが一致するパケットを取得 + List allPackets = Packets.getInstance().queryAll(); + List jobPackets = new ArrayList<>(); + + log("JobStatusTool: Searching for job " + jobId + " in " + allPackets.size() + " total packets"); + + for (Packet packet : allPackets) { + String packetJobId = packet.getJobId(); + if (packetJobId != null) { + log("JobStatusTool: Packet " + packet.getId() + " has job_id: " + packetJobId); + } + if (jobId.equals(packetJobId)) { + jobPackets.add(packet); + log("JobStatusTool: Found matching packet " + packet.getId() + " for job " + jobId); + } + } + + log("JobStatusTool: Found " + jobPackets.size() + " packets for job " + jobId); + + if (jobPackets.isEmpty()) { + throw new IllegalArgumentException("Job not found: " + jobId); + } + + // temporary_id ごとにパケットを整理 + Map jobRequests = new HashMap<>(); + + for (Packet packet : jobPackets) { + String temporaryId = packet.getTemporaryId(); + if (temporaryId == null || temporaryId.trim().isEmpty()) { + continue; + } + + JobRequest jobRequest = jobRequests.computeIfAbsent(temporaryId, k -> new JobRequest()); + jobRequest.temporaryId = temporaryId; + + if (packet.getDirection() == Packet.Direction.CLIENT) { + // リクエストパケット + jobRequest.requestPacketId = packet.getId(); + jobRequest.hasRequest = true; + } else if (packet.getDirection() == Packet.Direction.SERVER) { + // レスポンスパケット + jobRequest.responsePacketId = packet.getId(); + jobRequest.hasResponse = true; + } + } + + // 結果を構築 + JsonObject result = new JsonObject(); + result.addProperty("job_id", jobId); + result.addProperty("total_requests", jobRequests.size()); + + int requestsSent = 0; + int responsesReceived = 0; + + for (JobRequest jobRequest : jobRequests.values()) { + if (jobRequest.hasRequest) { + requestsSent++; + } + if (jobRequest.hasResponse) { + responsesReceived++; + } + } + + result.addProperty("requests_sent", requestsSent); + result.addProperty("responses_received", responsesReceived); + + // ジョブの状態を判定 + String status; + if (requestsSent == 0) { + status = "created"; + } else if (requestsSent < jobRequests.size()) { + status = "sending_requests"; + } else if (responsesReceived == 0) { + status = "requests_sent"; + } else if (responsesReceived < requestsSent) { + status = "receiving_responses"; + } else { + status = "completed"; + } + result.addProperty("status", status); + + // 各リクエストの詳細 + JsonArray requestsArray = new JsonArray(); + for (JobRequest jobRequest : jobRequests.values()) { + JsonObject reqObj = new JsonObject(); + reqObj.addProperty("temporary_id", jobRequest.temporaryId); + reqObj.addProperty("has_request", jobRequest.hasRequest); + reqObj.addProperty("has_response", jobRequest.hasResponse); + + if (jobRequest.hasRequest) { + reqObj.addProperty("request_packet_id", jobRequest.requestPacketId); + } + if (jobRequest.hasResponse) { + reqObj.addProperty("response_packet_id", jobRequest.responsePacketId); + } + + requestsArray.add(reqObj); + } + result.add("requests", requestsArray); + + log("JobStatusTool: Job " + jobId + " has " + requestsSent + " requests sent, " + responsesReceived + + " responses received, status: " + status); + + return result; + } + + /** + * 全ジョブの概要を取得 + */ + private JsonObject getAllJobsStatus() throws Exception { + log("JobStatusTool: Getting status for all jobs"); + + // 全パケットからjob_idが設定されているものを取得 + List allPackets = Packets.getInstance().queryAll(); + Map jobs = new HashMap<>(); + + log("JobStatusTool: Total packets in database: " + allPackets.size()); + + int packetsWithJobId = 0; + for (Packet packet : allPackets) { + String jobId = packet.getJobId(); + if (jobId == null || jobId.trim().isEmpty()) { + continue; + } + + packetsWithJobId++; + log("JobStatusTool: Found packet " + packet.getId() + " with job_id: " + jobId + ", temporary_id: " + + packet.getTemporaryId()); + + JobSummary jobSummary = jobs.computeIfAbsent(jobId, k -> new JobSummary()); + jobSummary.jobId = jobId; + + String temporaryId = packet.getTemporaryId(); + if (temporaryId != null && !temporaryId.trim().isEmpty()) { + jobSummary.temporaryIds.add(temporaryId); + + if (packet.getDirection() == Packet.Direction.CLIENT) { + jobSummary.requestsSent++; + } else if (packet.getDirection() == Packet.Direction.SERVER) { + jobSummary.responsesReceived++; + } + } + } + + log("JobStatusTool: Found " + packetsWithJobId + " packets with job_id"); + + // 結果を構築 + JsonObject result = new JsonObject(); + result.addProperty("total_jobs", jobs.size()); + + JsonArray jobsArray = new JsonArray(); + for (JobSummary jobSummary : jobs.values()) { + JsonObject jobObj = new JsonObject(); + jobObj.addProperty("job_id", jobSummary.jobId); + jobObj.addProperty("total_requests", jobSummary.temporaryIds.size()); + jobObj.addProperty("requests_sent", jobSummary.requestsSent); + jobObj.addProperty("responses_received", jobSummary.responsesReceived); + + // ステータスを判定 + String status; + if (jobSummary.requestsSent == 0) { + status = "created"; + } else if (jobSummary.responsesReceived == 0) { + status = "requests_sent"; + } else if (jobSummary.responsesReceived < jobSummary.requestsSent) { + status = "receiving_responses"; + } else { + status = "completed"; + } + jobObj.addProperty("status", status); + + jobsArray.add(jobObj); + } + result.add("jobs", jobsArray); + + log("JobStatusTool: Found " + jobs.size() + " jobs"); + return result; + } + + /** + * ジョブのリクエスト情報 + */ + private static class JobRequest { + String temporaryId; + boolean hasRequest = false; + boolean hasResponse = false; + int requestPacketId = -1; + int responsePacketId = -1; + } + + /** + * ジョブの概要情報 + */ + private static class JobSummary { + String jobId; + List temporaryIds = new ArrayList<>(); + int requestsSent = 0; + int responsesReceived = 0; + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java index 58991c41..ebcfa5bf 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java @@ -8,8 +8,6 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; import java.util.Random; import java.util.UUID; import java.util.regex.Matcher; @@ -172,6 +170,9 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception log("ResendPacketTool: Original packet found, preparing for resend"); + // ジョブIDを生成 + String jobId = UUID.randomUUID().toString(); + long startTime = System.currentTimeMillis(); int sentCount = 0; int failedCount = 0; @@ -182,8 +183,21 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception if (modifications.size() == 0) { // 改変なしの場合は単純再送 log("ResendPacketTool: Simple resend without modifications, count=" + count); - resendController.resend(originalOneShot, count); - sentCount = count; + for (int i = 0; i < count; i++) { + String temporaryId = UUID.randomUUID().toString(); + OneShotPacket jobPacket = new OneShotPacket(originalOneShot.getId(), + originalOneShot.getListenPort(), originalOneShot.getClient(), originalOneShot.getServer(), + originalOneShot.getServerName(), originalOneShot.getUseSSL(), originalOneShot.getData(), + originalOneShot.getEncoder(), originalOneShot.getAlpn(), originalOneShot.getDirection(), + originalOneShot.getConn(), originalOneShot.getGroup(), jobId, temporaryId); + resendController.resend(jobPacket); + sentCount++; + + // インターバル待機(最後の送信後は待機しない) + if (intervalMs > 0 && i < count - 1) { + Thread.sleep(intervalMs); + } + } } else { // 複数回送信または改変ありの場合 log("ResendPacketTool: Complex resend with count=" + count + " and modifications=" @@ -191,8 +205,9 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception for (int i = 0; i < count; i++) { try { + String temporaryId = UUID.randomUUID().toString(); OneShotPacket modifiedPacket = applyModifications(originalOneShot, modifications, i + 1, - allowDuplicateHeaders); + allowDuplicateHeaders, jobId, temporaryId); resendController.resend(modifiedPacket); sentCount++; @@ -220,6 +235,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception result.addProperty("sent_count", sentCount); result.addProperty("failed_count", failedCount); result.addProperty("execution_time_ms", executionTime); + result.addProperty("job_id", jobId); log("ResendPacketTool: Completed. Sent: " + sentCount + ", Failed: " + failedCount + ", Time: " + executionTime + "ms"); @@ -230,7 +246,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception * パケットに改変を適用 */ private OneShotPacket applyModifications(OneShotPacket original, JsonArray modifications, int index, - boolean allowDuplicateHeaders) throws Exception { + boolean allowDuplicateHeaders, String jobId, String temporaryId) throws Exception { if (modifications.size() == 0) { return original; } @@ -270,7 +286,7 @@ private OneShotPacket applyModifications(OneShotPacket original, JsonArray modif OneShotPacket modifiedPacket = new OneShotPacket(original.getId(), original.getListenPort(), original.getClient(), original.getServer(), original.getServerName(), original.getUseSSL(), data, original.getEncoder(), original.getAlpn(), original.getDirection(), original.getConn(), - original.getGroup()); + original.getGroup(), jobId, temporaryId); return modifiedPacket; } diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java index 850edabb..fb1d6130 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java @@ -25,6 +25,7 @@ private void registerDefaultTools() { registerTool(new ResendPacketTool()); registerTool(new BulkSendTool()); registerTool(new VulCheckHelperTool()); + registerTool(new JobStatusTool()); } public void registerTool(MCPTool tool) { @@ -57,23 +58,23 @@ public JsonObject callTool(String toolName, JsonObject arguments) throws Excepti } JsonObject toolResult = tool.call(arguments); - + // MCP仕様に準拠した応答形式に変換 JsonObject mcpResponse = new JsonObject(); - + // content配列を作成 (必須) JsonArray content = new JsonArray(); JsonObject textContent = new JsonObject(); textContent.addProperty("type", "text"); textContent.addProperty("text", toolResult.toString()); content.add(textContent); - + mcpResponse.add("content", content); mcpResponse.addProperty("isError", false); - + // 元の結果をstructuredContentとして保持(オプション) mcpResponse.add("structuredContent", toolResult); - + return mcpResponse; } } diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java index 4749ccdb..e05dbf0c 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import packetproxy.VulCheckerManager; @@ -190,6 +191,9 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception "VulCheck type '" + vulCheckType + "' not found. Use 'list' to see available types."); } + // ジョブIDを生成 + String jobId = UUID.randomUUID().toString(); + // ターゲット位置を解析 List targetLocations = parseTargetLocations(targetLocationsJson, originalOneShot.getData()); @@ -197,7 +201,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception // VulCheckテストを実行 VulCheckResult result = executeVulCheckTests(originalOneShot, vulChecker, targetLocations, intervalMs, mode, - maxPayloads, timeoutMs); + maxPayloads, timeoutMs, jobId); long executionTime = System.currentTimeMillis() - startTime; @@ -211,6 +215,7 @@ protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception response.addProperty("total_packets_sent", result.totalPacketsSent); response.addProperty("total_failed", result.totalFailed); response.addProperty("execution_time_ms", executionTime); + response.addProperty("job_id", jobId); // 各ターゲット位置の結果 JsonArray locationResults = new JsonArray(); @@ -373,8 +378,8 @@ private List parseTargetLocations(JsonArray targetLocations, byt * VulCheckテストを実行 */ private VulCheckResult executeVulCheckTests(OneShotPacket originalPacket, VulChecker vulChecker, - List targetLocations, int intervalMs, String mode, int maxPayloads, int timeoutMs) - throws Exception { + List targetLocations, int intervalMs, String mode, int maxPayloads, int timeoutMs, + String jobId) throws Exception { VulCheckResult result = new VulCheckResult(); result.overallSuccess = true; @@ -388,7 +393,7 @@ private VulCheckResult executeVulCheckTests(OneShotPacket originalPacket, VulChe + targetLocation.range.getPositionEnd() + " (" + targetLocation.description + ")"); LocationResult locResult = processTargetLocation(originalPacket, vulChecker, targetLocation, intervalMs, - mode, maxPayloads); + mode, maxPayloads, jobId); result.locationResults.add(locResult); result.totalPayloadsGenerated += locResult.payloadsGenerated; @@ -412,7 +417,8 @@ private VulCheckResult executeVulCheckTests(OneShotPacket originalPacket, VulChe * 特定のターゲット位置でVulCheckテストを実行 */ private LocationResult processTargetLocation(OneShotPacket originalPacket, VulChecker vulChecker, - TargetLocation targetLocation, int intervalMs, String mode, int maxPayloads) throws Exception { + TargetLocation targetLocation, int intervalMs, String mode, int maxPayloads, String jobId) + throws Exception { LocationResult result = new LocationResult(); result.range = targetLocation.range; @@ -486,11 +492,13 @@ private LocationResult processTargetLocation(OneShotPacket originalPacket, VulCh byte[] modifiedData = modifiedText.getBytes(); + // ジョブ情報を付与してパケット作成 + String temporaryId = UUID.randomUUID().toString(); OneShotPacket modifiedPacket = new OneShotPacket(originalPacket.getId(), originalPacket.getListenPort(), originalPacket.getClient(), originalPacket.getServer(), originalPacket.getServerName(), originalPacket.getUseSSL(), modifiedData, originalPacket.getEncoder(), originalPacket.getAlpn(), originalPacket.getDirection(), - originalPacket.getConn(), originalPacket.getGroup()); + originalPacket.getConn(), originalPacket.getGroup(), jobId, temporaryId); // 送信モードに応じて処理 if ("parallel".equals(mode)) { diff --git a/src/main/java/core/packetproxy/model/OneShotPacket.java b/src/main/java/core/packetproxy/model/OneShotPacket.java index 8c199a7d..ef241284 100644 --- a/src/main/java/core/packetproxy/model/OneShotPacket.java +++ b/src/main/java/core/packetproxy/model/OneShotPacket.java @@ -42,6 +42,8 @@ public class OneShotPacket implements PacketInfo, Cloneable { private boolean auto_modified; private int conn; private long group; + private String job_id; + private String temporary_id; public OneShotPacket() { } @@ -51,7 +53,15 @@ public OneShotPacket(int id, int listen_port, InetSocketAddress client_addr, Ine int conn, long group) { initialize(id, listen_port, client_addr.getAddress().getHostAddress(), client_addr.getPort(), server_addr.getAddress().getHostAddress(), server_addr.getPort(), server_name, use_ssl, data, - encoder_name, alpn, dir, conn, group); + encoder_name, alpn, dir, conn, group, null, null); + } + + public OneShotPacket(int id, int listen_port, InetSocketAddress client_addr, InetSocketAddress server_addr, + String server_name, boolean use_ssl, byte[] data, String encoder_name, String alpn, Packet.Direction dir, + int conn, long group, String job_id, String temporary_id) { + initialize(id, listen_port, client_addr.getAddress().getHostAddress(), client_addr.getPort(), + server_addr.getAddress().getHostAddress(), server_addr.getPort(), server_name, use_ssl, data, + encoder_name, alpn, dir, conn, group, job_id, temporary_id); } @Override @@ -61,7 +71,7 @@ public Object clone() throws CloneNotSupportedException { private void initialize(int id, int listen_port, String client_ip, int client_port, String server_ip, int server_port, String server_name, boolean use_ssl, byte[] data, String encoder_name, String alpn, - Packet.Direction dir, int conn, long group) { + Packet.Direction dir, int conn, long group, String job_id, String temporary_id) { this.id = id; this.listen_port = listen_port; this.client_ip = client_ip; @@ -77,6 +87,8 @@ private void initialize(int id, int listen_port, String client_ip, int client_po this.auto_modified = false; this.conn = conn; this.group = group; + this.job_id = job_id; + this.temporary_id = temporary_id; } public Packet.Direction getDirection() { @@ -175,6 +187,22 @@ public long getGroup() { return this.group; } + public String getJobId() { + return this.job_id; + } + + public void setJobId(String job_id) { + this.job_id = job_id; + } + + public String getTemporaryId() { + return this.temporary_id; + } + + public void setTemporaryId(String temporary_id) { + this.temporary_id = temporary_id; + } + public void encode() { } @@ -182,6 +210,8 @@ public Packet toPacket() throws Exception { Packet packet = new Packet(listen_port, client_ip, client_port, server_ip, server_port, server_name, use_ssl, encoder_name, alpn, direction, conn, group); packet.setDecodedData(getData()); + packet.setJobId(job_id); + packet.setTemporaryId(temporary_id); return packet; } diff --git a/src/main/java/core/packetproxy/model/Packet.java b/src/main/java/core/packetproxy/model/Packet.java index 9f28f2f9..5e06274f 100644 --- a/src/main/java/core/packetproxy/model/Packet.java +++ b/src/main/java/core/packetproxy/model/Packet.java @@ -76,6 +76,10 @@ public enum Direction { private long group; @DatabaseField private String color; + @DatabaseField + private String job_id; + @DatabaseField + private String temporary_id; public Packet() { // ORMLite needs a no-arg constructor @@ -128,7 +132,7 @@ public int getId() { public OneShotPacket getOneShotPacket(byte[] data) { return new OneShotPacket(getId(), getListenPort(), getClient(), getServer(), getServerName(), getUseSSL(), data, - getEncoder(), getAlpn(), getDirection(), getConn(), getGroup()); + getEncoder(), getAlpn(), getDirection(), getConn(), getGroup(), getJobId(), getTemporaryId()); } @@ -142,7 +146,8 @@ public byte[] getModifiedData() { public OneShotPacket getOneShotFromModifiedData() { return new OneShotPacket(getId(), getListenPort(), getClient(), getServer(), getServerName(), getUseSSL(), - getModifiedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup()); + getModifiedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup(), getJobId(), + getTemporaryId()); } public void setSentData(byte[] data) { @@ -163,7 +168,8 @@ public byte[] getReceivedData() { public OneShotPacket getOneShotFromReceivedData() { return new OneShotPacket(getId(), getListenPort(), getClient(), getServer(), getServerName(), getUseSSL(), - getReceivedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup()); + getReceivedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup(), getJobId(), + getTemporaryId()); } public void setDecodedData(byte[] data) { @@ -176,7 +182,8 @@ public byte[] getDecodedData() { public OneShotPacket getOneShotFromDecodedData() { return new OneShotPacket(getId(), getListenPort(), getClient(), getServer(), getServerName(), getUseSSL(), - getDecodedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup()); + getDecodedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup(), getJobId(), + getTemporaryId()); } public void setModified() { @@ -271,6 +278,22 @@ public void setColor(String color) { this.color = color; } + public String getJobId() { + return this.job_id; + } + + public void setJobId(String job_id) { + this.job_id = job_id; + } + + public String getTemporaryId() { + return this.temporary_id; + } + + public void setTemporaryId(String temporary_id) { + this.temporary_id = temporary_id; + } + public String getSummarizedRequest() throws Exception { Encoder encoder = EncoderManager.getInstance().createInstance(encoder_name, null); if (encoder == null) { diff --git a/src/main/java/core/packetproxy/model/Packets.java b/src/main/java/core/packetproxy/model/Packets.java index 8a4730cb..0a89fa77 100644 --- a/src/main/java/core/packetproxy/model/Packets.java +++ b/src/main/java/core/packetproxy/model/Packets.java @@ -236,6 +236,13 @@ public void handleDatabaseMessage(DatabaseMessage message) { dao.executeRaw("ALTER TABLE `packets` ADD COLUMN color VARCHAR"); } + // job_idカラムとtemporary_idカラムも追加する + if (!result.contains("`job_id` VARCHAR")) { + dao.executeRaw("ALTER TABLE `packets` ADD COLUMN job_id VARCHAR"); + } + if (!result.contains("`temporary_id` VARCHAR")) { + dao.executeRaw("ALTER TABLE `packets` ADD COLUMN temporary_id VARCHAR"); + } firePropertyChange(message); break; case RECREATE : @@ -255,7 +262,7 @@ private boolean isLatestVersion() throws Exception { String result = dao.queryRaw("SELECT sql FROM sqlite_master WHERE name='packets'").getFirstResult()[0]; // System.out.println(result); return result.equals( - "CREATE TABLE `packets` (`id` INTEGER PRIMARY KEY AUTOINCREMENT , `direction` VARCHAR , `decoded_data` BLOB , `modified_data` BLOB , `sent_data` BLOB , `received_data` BLOB , `listen_port` INTEGER , `client_ip` VARCHAR , `client_port` INTEGER , `server_ip` VARCHAR , `server_name` VARCHAR , `server_port` INTEGER , `use_ssl` BOOLEAN , `content_type` VARCHAR , `encoder_name` VARCHAR , `alpn` VARCHAR , `modified` BOOLEAN , `resend` BOOLEAN , `date` BIGINT , `conn` INTEGER , `group` BIGINT , `color` VARCHAR )"); + "CREATE TABLE `packets` (`id` INTEGER PRIMARY KEY AUTOINCREMENT , `direction` VARCHAR , `decoded_data` BLOB , `modified_data` BLOB , `sent_data` BLOB , `received_data` BLOB , `listen_port` INTEGER , `client_ip` VARCHAR , `client_port` INTEGER , `server_ip` VARCHAR , `server_name` VARCHAR , `server_port` INTEGER , `use_ssl` BOOLEAN , `content_type` VARCHAR , `encoder_name` VARCHAR , `alpn` VARCHAR , `modified` BOOLEAN , `resend` BOOLEAN , `date` BIGINT , `conn` INTEGER , `group` BIGINT , `color` VARCHAR , `job_id` VARCHAR , `temporary_id` VARCHAR )"); } private void RecreateTable() throws Exception { From 59d0335d7cb902aff5fcf9ecaa34c846a4613547 Mon Sep 17 00:00:00 2001 From: kakira Date: Wed, 13 Aug 2025 11:41:59 +0900 Subject: [PATCH 38/39] =?UTF-8?q?call=5Fvulcheck=5Fhelper=20=E3=81=8C?= =?UTF-8?q?=E7=84=A1=E9=A7=84=E3=81=AB=E5=91=BC=E3=81=B3=E5=87=BA=E3=81=95?= =?UTF-8?q?=E3=82=8C=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB=E8=AA=AC?= =?UTF-8?q?=E6=98=8E=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/mcp-server-spec.md | 2 ++ .../packetproxy/extensions/mcp/tools/VulCheckHelperTool.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md index 36d8ec26..099c9c2d 100644 --- a/doc/mcp-server-spec.md +++ b/doc/mcp-server-spec.md @@ -729,6 +729,8 @@ IMPORTANT: Requires a complete configuration object, not partial updates. 指定されたパケットにVulCheckテストケースを適用して、自動的にペイロードを生成し、指定された位置に注入してバッチ送信を実行します。 +**重要な制限事項**: 現在、NumberとJWTの脆弱性タイプのみをサポートしています。その他の脆弱性診断については、`bulk_send`や`resend_packet`ツールを使用してください。 + **リクエスト:** ```json diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java index e05dbf0c..3d0a44aa 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java @@ -34,7 +34,7 @@ public String getName() { @Override public String getDescription() { - return "Execute VulCheck vulnerability tests with automatic payload generation and batch sending. Applies VulCheck test cases to specified packet locations and sends modified packets with configurable intervals. IMPORTANT: For precise targeting, use regex patterns with positive lookahead assertions. Examples: To target '17' in 'X-Version: 17.0.4', use pattern: '17(?=\\.0\\.4)'. To target '123' in 'userId=123&other=456', use pattern: '(?<=userId=)123(?=&)'. To target specific values while preserving structure, use patterns like: 'sessionId=\\\\w+' with replacement 'sessionId=$1'. The pattern field supports full regex syntax including lookahead/lookbehind assertions for precise matching without affecting surrounding text. Use replacement field to control how the matched text is substituted with VulCheck payloads."; + return "Execute VulCheck vulnerability tests with automatic payload generation and batch sending. Applies VulCheck test cases to specified packet locations and sends modified packets with configurable intervals. IMPORTANT: Currently supports Number and JWT vulnerability types only. For other vulnerability types, use bulk_send or resend_packet tools instead. For precise targeting, use regex patterns with positive lookahead assertions. Examples: To target '17' in 'X-Version: 17.0.4', use pattern: '17(?=\\.0\\.4)'. To target '123' in 'userId=123&other=456', use pattern: '(?<=userId=)123(?=&)'. To target specific values while preserving structure, use patterns like: 'sessionId=\\\\w+' with replacement 'sessionId=$1'. The pattern field supports full regex syntax including lookahead/lookbehind assertions for precise matching without affecting surrounding text. Use replacement field to control how the matched text is substituted with VulCheck payloads."; } @Override From 35f3ca421885d831a7a4f544556c22683116f39c Mon Sep 17 00:00:00 2001 From: kakira Date: Wed, 13 Aug 2025 11:49:01 +0900 Subject: [PATCH 39/39] =?UTF-8?q?Server=20Stop=20=E6=99=82=E3=81=AB?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=8C=E7=99=BA=E7=94=9F=E3=81=97?= =?UTF-8?q?=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/packetproxy/extensions/mcp/MCPServer.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java index 704c36ce..96578c1b 100644 --- a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java @@ -60,16 +60,8 @@ public void run() throws IOException { public void stop() { running = false; - try { - if (reader != null) { - reader.close(); - } - if (writer != null) { - writer.close(); - } - } catch (IOException e) { - logger.accept("Error closing streams: " + e.getMessage()); - } + // Don't close System.in/System.out streams as they are global + // Just mark as stopped - the run() loop will break on next read } public JsonObject processTestRequest(JsonObject request) throws Exception {