diff --git a/.idea/claudeCodeTabState.xml b/.idea/claudeCodeTabState.xml
new file mode 100644
index 0000000..be38166
--- /dev/null
+++ b/.idea/claudeCodeTabState.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/php.xml b/.idea/php.xml
index 4c1f8cf..3e486c0 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -43,7 +43,7 @@
-
+
diff --git a/composer.json b/composer.json
index cbf4714..50ae81a 100644
--- a/composer.json
+++ b/composer.json
@@ -1,5 +1,6 @@
{
"require": {
+ "php": "^8.2",
"mcp/sdk": "^0.6.0",
"guzzlehttp/guzzle": "^7.12"
},
diff --git a/composer.lock b/composer.lock
index d22e5cb..37817c7 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "90853eb8e03275ba165d4875872d9169",
+ "content-hash": "28614624427adcf2fb81f8d8c2311fff",
"packages": [
{
"name": "doctrine/deprecations",
@@ -1868,7 +1868,9 @@
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
- "platform": {},
+ "platform": {
+ "php": "^8.2"
+ },
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
diff --git a/server.php b/server.php
index df7168f..7e793ec 100644
--- a/server.php
+++ b/server.php
@@ -4,142 +4,7 @@ declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
-use GuzzleHttp\Client;
-use Mcp\Server;
+use App\Server\ServerFactory;
use Mcp\Server\Transport\StdioTransport;
-$token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiI2ODI5N2FjYzUwZDczYWQzZmNmMTNlM2Y4MWYxMWQyZSIsImlzcyI6InRwb2ludC5hZ3JpbWVkaWEuY24iLCJhdWQiOiJ0cG9pbnQuYWdyaW1lZGlhLmNuIiwiaWF0IjoxNzgwODk3Mjk3LCJuYmYiOjE3ODA4OTcyOTcsImV4cCI6MTc4MzQwMjg5NywianRpIjp7ImlkIjo0LCJ0eXBlIjoiYWRtaW4ifX0.G2yB38bYkf0b37kpAbQViDuXY4QHeRjm99WNXjSh4L0';
-
-$server = Server::builder()
- ->setServerInfo('Merchant MCP', '1.0.0')
- ->addTool(
- handler: function (
- ?string $date = null,
- ?int $status = null,
- ?string $keyword = null,
- ?string $fieldKey = null,
- ?string $payType = null,
- ?string $type = null,
- ?int $btcId = null,
- ) use ($token): array {
- $client = new Client([
- 'timeout' => 10,
- ]);
-
- // page、limit 由 MCP 内部固定,不开放给 AI 配置
- $query = [
- 'status' => $status ?? -1,
- 'field_key' => $fieldKey ?? 'all',
- 'page' => 1,
- 'limit' => 100,
- 'time' => $date ?? date('Y/m/d') . '-' . date('Y/m/d'),
- ];
-
- // 仅在传入非空值时追加可选筛选条件
- // 注意:API 搜索关键词参数是 real_name,不是 data
- if ($keyword !== null && $keyword !== '') {
- $query['real_name'] = $keyword;
- }
- if ($payType !== null && $payType !== '') {
- $query['pay_type'] = $payType;
- }
- if ($type !== null && $type !== '') {
- $query['type'] = $type;
- }
- if ($btcId !== null && $btcId > 0) {
- $query['btc_id'] = $btcId;
- }
-
- $response = $client->get(
- 'https://tpoint.agrimedia.cn/adminapi/offline/list',
- [
- 'headers' => [
- 'accept' => 'application/json',
- 'authori-zation' => 'Bearer ' . $token,
- ],
- 'query' => $query,
- ]
- );
-
- $data = json_decode(
- (string) $response->getBody(),
- true
- );
-
- $orders = $data['data']['list'] ?? [];
-
- $activeMerchants = [];
- $successOrderCount = 0;
- $totalOrderAmount = 0.0;
- $totalRealPay = 0.0;
-
- foreach ($orders as $order) {
- $merchantId = (int) ($order['merchant_id'] ?? 0);
-
- if ($merchantId > 0) {
- $activeMerchants[$merchantId] = true;
- }
-
- /*
- * 这里暂时按 status = 1 作为成功订单。
- * 后面再确认你项目的真实状态含义。
- */
- if ((int) ($order['status'] ?? -1) === 1) {
- $successOrderCount++;
- }
-
- $totalOrderAmount += (float) ($order['order_price'] ?? 0);
- $totalRealPay += (float) ($order['real_pay'] ?? 0);
- }
-
- return [
- 'orderCount' => count($orders),
- 'activeMerchantCount' => count($activeMerchants),
- 'successOrderCount' => $successOrderCount,
- 'totalOrderAmount' => round($totalOrderAmount, 2),
- 'totalRealPay' => round($totalRealPay, 2),
- // 调试信息:实际发送给 API 的查询参数
- '_debug_query' => $query,
- ];
- },
- name: 'get_order_summary',
- description: '获取订单总数、活跃商户数、成功订单数和金额统计。可按日期、状态、关键词、搜索字段、支付方式、订单类型、商户分类ID筛选。',
- inputSchema: [
- 'type' => 'object',
- 'properties' => [
- 'date' => [
- 'type' => 'string',
- 'description' => '查询日期,格式如 2026/06/23-2026/06/23(起止日期用 - 连接)。不传默认当天。',
- ],
- 'status' => [
- 'type' => 'integer',
- 'description' => '订单状态,-1 表示全部。不传默认 -1。',
- ],
- 'fieldKey' => [
- 'type' => 'string',
- 'description' => '搜索字段,对应接口的 field_key 参数。常用值:order_sn(按订单号搜索)、all(全部字段)。不传默认 all。注意:当 keyword 有值时,建议同时指定 fieldKey 以确保搜索生效。',
- ],
- 'keyword' => [
- 'type' => 'string',
- 'description' => '搜索关键词,对应接口的 data 参数。需配合 fieldKey 一起使用。',
- ],
- 'payType' => [
- 'type' => 'string',
- 'description' => '支付方式,对应接口的 pay_type 参数。',
- ],
- 'type' => [
- 'type' => 'string',
- 'description' => '订单类型,对应接口的 type 参数。',
- ],
- 'btcId' => [
- 'type' => 'integer',
- 'description' => '商户分类ID,对应接口的 btc_id 参数。',
- ],
- ],
- ],
- )
- ->build();
-
-$transport = new StdioTransport();
-
-$server->run($transport);
\ No newline at end of file
+ServerFactory::create()->run(new StdioTransport());
diff --git a/src/Config/ApiConfig.php b/src/Config/ApiConfig.php
new file mode 100644
index 0000000..7241d8b
--- /dev/null
+++ b/src/Config/ApiConfig.php
@@ -0,0 +1,32 @@
+ 0 ? $timeout : 10,
+ );
+ }
+}
diff --git a/src/DemoTools.php b/src/DemoTools.php
deleted file mode 100644
index 1267a0e..0000000
--- a/src/DemoTools.php
+++ /dev/null
@@ -1,19 +0,0 @@
-client = new Client([
+ 'base_uri' => $this->config->baseUri,
+ 'timeout' => $this->config->timeout,
+ ]);
+ }
+
+ /**
+ * @param array $query
+ * @return array
+ */
+ public function getOfflineOrders(array $query): array
+ {
+ try {
+ $response = $this->client->get('/adminapi/offline/list', [
+ 'headers' => [
+ 'accept' => 'application/json',
+ 'authori-zation' => 'Bearer ' . $this->config->token,
+ ],
+ 'query' => $query,
+ ]);
+ } catch (GuzzleException $exception) {
+ throw new \RuntimeException('Merchant API request failed: ' . $exception->getMessage(), 0, $exception);
+ }
+
+ $data = json_decode((string) $response->getBody(), true);
+
+ if (!is_array($data)) {
+ throw new \RuntimeException('Merchant API returned invalid JSON.');
+ }
+
+ return $data;
+ }
+}
diff --git a/src/Repositories/OfflineOrderRepository.php b/src/Repositories/OfflineOrderRepository.php
new file mode 100644
index 0000000..894181f
--- /dev/null
+++ b/src/Repositories/OfflineOrderRepository.php
@@ -0,0 +1,30 @@
+ $query
+ * @return list>
+ */
+ public function list(array $query): array
+ {
+ $data = $this->apiClient->getOfflineOrders($query);
+ $orders = $data['data']['list'] ?? [];
+
+ if (!is_array($orders)) {
+ return [];
+ }
+
+ return array_values(array_filter($orders, is_array(...)));
+ }
+}
diff --git a/src/Server/ServerFactory.php b/src/Server/ServerFactory.php
new file mode 100644
index 0000000..25c902b
--- /dev/null
+++ b/src/Server/ServerFactory.php
@@ -0,0 +1,34 @@
+setServerInfo('Merchant MCP', '1.0.0');
+
+ return $registrar->register($builder)->build();
+ }
+}
diff --git a/src/Support/OfflineOrderQuery.php b/src/Support/OfflineOrderQuery.php
new file mode 100644
index 0000000..ad1c1d3
--- /dev/null
+++ b/src/Support/OfflineOrderQuery.php
@@ -0,0 +1,54 @@
+
+ */
+ public static function build(
+ ?string $date = null,
+ ?int $status = null,
+ ?string $keyword = null,
+ ?string $fieldKey = null,
+ ?string $payType = null,
+ ?string $type = null,
+ ?int $btcId = null,
+ int $page = 1,
+ int $limit = 100,
+ ): array {
+ $query = [
+ 'status' => $status ?? -1,
+ 'field_key' => self::filled($fieldKey) ? $fieldKey : 'all',
+ 'page' => $page,
+ 'limit' => $limit,
+ 'time' => self::filled($date) ? $date : date('Y/m/d') . '-' . date('Y/m/d'),
+ ];
+
+ if (self::filled($keyword)) {
+ $query['real_name'] = $keyword;
+ }
+
+ if (self::filled($payType)) {
+ $query['pay_type'] = $payType;
+ }
+
+ if (self::filled($type)) {
+ $query['type'] = $type;
+ }
+
+ if ($btcId !== null && $btcId > 0) {
+ $query['btc_id'] = $btcId;
+ }
+
+ return $query;
+ }
+
+ private static function filled(?string $value): bool
+ {
+ return $value !== null && trim($value) !== '';
+ }
+}
diff --git a/src/Tools/AnalyzeOfflineOrdersTool.php b/src/Tools/AnalyzeOfflineOrdersTool.php
new file mode 100644
index 0000000..cb5b95f
--- /dev/null
+++ b/src/Tools/AnalyzeOfflineOrdersTool.php
@@ -0,0 +1,78 @@
+orders->list($query);
+
+ $statusDistribution = [];
+ $payTypeDistribution = [];
+ $typeDistribution = [];
+ $totalOrderAmount = 0.0;
+ $totalRealPay = 0.0;
+ $maxOrderAmount = 0.0;
+
+ foreach ($orders as $order) {
+ $statusKey = (string) ($order['status'] ?? 'unknown');
+ $payTypeKey = (string) ($order['pay_type'] ?? 'unknown');
+ $typeKey = (string) ($order['type'] ?? 'unknown');
+ $orderAmount = (float) ($order['order_price'] ?? 0);
+ $realPay = (float) ($order['real_pay'] ?? 0);
+
+ $statusDistribution[$statusKey] = ($statusDistribution[$statusKey] ?? 0) + 1;
+ $payTypeDistribution[$payTypeKey] = ($payTypeDistribution[$payTypeKey] ?? 0) + 1;
+ $typeDistribution[$typeKey] = ($typeDistribution[$typeKey] ?? 0) + 1;
+ $totalOrderAmount += $orderAmount;
+ $totalRealPay += $realPay;
+ $maxOrderAmount = max($maxOrderAmount, $orderAmount);
+ }
+
+ $orderCount = count($orders);
+
+ return [
+ 'orderCount' => $orderCount,
+ 'statusDistribution' => $statusDistribution,
+ 'payTypeDistribution' => $payTypeDistribution,
+ 'typeDistribution' => $typeDistribution,
+ 'totalOrderAmount' => round($totalOrderAmount, 2),
+ 'totalRealPay' => round($totalRealPay, 2),
+ 'averageOrderAmount' => $orderCount > 0 ? round($totalOrderAmount / $orderCount, 2) : 0,
+ 'averageRealPay' => $orderCount > 0 ? round($totalRealPay / $orderCount, 2) : 0,
+ 'maxOrderAmount' => round($maxOrderAmount, 2),
+ '_debug_query' => $query,
+ ];
+ }
+
+ public static function inputSchema(): array
+ {
+ return [
+ 'type' => 'object',
+ 'properties' => OrderSummaryTool::filterProperties(),
+ ];
+ }
+
+}
diff --git a/src/Tools/MerchantRankingTool.php b/src/Tools/MerchantRankingTool.php
new file mode 100644
index 0000000..a843a29
--- /dev/null
+++ b/src/Tools/MerchantRankingTool.php
@@ -0,0 +1,113 @@
+orders->list($query);
+ $ranking = [];
+
+ foreach ($orders as $order) {
+ $merchantId = (int) ($order['merchant_id'] ?? 0);
+
+ if ($merchantId <= 0) {
+ continue;
+ }
+
+ if (!isset($ranking[$merchantId])) {
+ $ranking[$merchantId] = [
+ 'merchantId' => $merchantId,
+ 'merchantName' => (string) ($order['merchant_name'] ?? $order['mer_name'] ?? ''),
+ 'orderCount' => 0,
+ 'successOrderCount' => 0,
+ 'totalOrderAmount' => 0.0,
+ 'totalRealPay' => 0.0,
+ ];
+ }
+
+ $ranking[$merchantId]['orderCount']++;
+ $ranking[$merchantId]['totalOrderAmount'] += (float) ($order['order_price'] ?? 0);
+ $ranking[$merchantId]['totalRealPay'] += (float) ($order['real_pay'] ?? 0);
+
+ if ((int) ($order['status'] ?? -1) === 1) {
+ $ranking[$merchantId]['successOrderCount']++;
+ }
+ }
+
+ $items = array_values($ranking);
+ $sortField = $this->normalizeSortField($sortBy);
+
+ usort($items, static function (array $left, array $right) use ($sortField): int {
+ return $right[$sortField] <=> $left[$sortField];
+ });
+
+ $maxItems = $limit !== null && $limit > 0 ? min($limit, 100) : 10;
+ $items = array_slice($items, 0, $maxItems);
+
+ foreach ($items as $index => $item) {
+ $items[$index]['totalOrderAmount'] = round((float) $item['totalOrderAmount'], 2);
+ $items[$index]['totalRealPay'] = round((float) $item['totalRealPay'], 2);
+ }
+
+ return [
+ 'sortBy' => $sortField,
+ 'ranking' => $items,
+ '_debug_query' => $query,
+ ];
+ }
+
+ public static function inputSchema(): array
+ {
+ $properties = OrderSummaryTool::filterProperties();
+ $properties['sortBy'] = [
+ 'type' => 'string',
+ 'description' => '排序字段:orderCount、successOrderCount、totalOrderAmount、totalRealPay。不传默认 totalRealPay。',
+ ];
+ $properties['limit'] = [
+ 'type' => 'integer',
+ 'description' => '返回商户数量,默认 10,最大 100。',
+ ];
+
+ return [
+ 'type' => 'object',
+ 'properties' => $properties,
+ ];
+ }
+
+ private function normalizeSortField(?string $sortBy): string
+ {
+ $allowed = ['orderCount', 'successOrderCount', 'totalOrderAmount', 'totalRealPay'];
+
+ if ($sortBy !== null && in_array($sortBy, $allowed, true)) {
+ return $sortBy;
+ }
+
+ return 'totalRealPay';
+ }
+
+}
diff --git a/src/Tools/OrderSummaryTool.php b/src/Tools/OrderSummaryTool.php
new file mode 100644
index 0000000..a6fa53e
--- /dev/null
+++ b/src/Tools/OrderSummaryTool.php
@@ -0,0 +1,107 @@
+orders->list($query);
+
+ $activeMerchants = [];
+ $successOrderCount = 0;
+ $totalOrderAmount = 0.0;
+ $totalRealPay = 0.0;
+
+ foreach ($orders as $order) {
+ $merchantId = (int) ($order['merchant_id'] ?? 0);
+
+ if ($merchantId > 0) {
+ $activeMerchants[$merchantId] = true;
+ }
+
+ if ((int) ($order['status'] ?? -1) === 1) {
+ $successOrderCount++;
+ }
+
+ $totalOrderAmount += (float) ($order['order_price'] ?? 0);
+ $totalRealPay += (float) ($order['real_pay'] ?? 0);
+ }
+
+ return [
+ 'orderCount' => count($orders),
+ 'activeMerchantCount' => count($activeMerchants),
+ 'successOrderCount' => $successOrderCount,
+ 'totalOrderAmount' => round($totalOrderAmount, 2),
+ 'totalRealPay' => round($totalRealPay, 2),
+ '_debug_query' => $query,
+ ];
+ }
+
+ public static function inputSchema(): array
+ {
+ return [
+ 'type' => 'object',
+ 'properties' => self::filterProperties(),
+ ];
+ }
+
+ /**
+ * @return array>
+ */
+ public static function filterProperties(): array
+ {
+ return [
+ 'date' => [
+ 'type' => 'string',
+ 'description' => '查询日期,格式如 2026/06/23-2026/06/23(起止日期用 - 连接)。不传默认当天。',
+ ],
+ 'status' => [
+ 'type' => 'integer',
+ 'description' => '订单状态,-1 表示全部。不传默认 -1。',
+ ],
+ 'fieldKey' => [
+ 'type' => 'string',
+ 'description' => '搜索字段,对应接口的 field_key 参数。常用值:order_sn(按订单号搜索)、all(全部字段)。不传默认 all。',
+ ],
+ 'keyword' => [
+ 'type' => 'string',
+ 'description' => '搜索关键词。当前接口按 real_name 参数传递。',
+ ],
+ 'payType' => [
+ 'type' => 'string',
+ 'description' => '支付方式,对应接口的 pay_type 参数。',
+ ],
+ 'type' => [
+ 'type' => 'string',
+ 'description' => '订单类型,对应接口的 type 参数。',
+ ],
+ 'btcId' => [
+ 'type' => 'integer',
+ 'description' => '商户分类ID,对应接口的 btc_id 参数。',
+ ],
+ ];
+ }
+
+}
diff --git a/src/Tools/ToolRegistrar.php b/src/Tools/ToolRegistrar.php
new file mode 100644
index 0000000..599672b
--- /dev/null
+++ b/src/Tools/ToolRegistrar.php
@@ -0,0 +1,81 @@
+addTool(
+ handler: $this->orderSummaryHandler(),
+ name: OrderSummaryTool::NAME,
+ description: OrderSummaryTool::DESCRIPTION,
+ inputSchema: OrderSummaryTool::inputSchema(),
+ )
+ ->addTool(
+ handler: $this->analyzeOfflineOrdersHandler(),
+ name: AnalyzeOfflineOrdersTool::NAME,
+ description: AnalyzeOfflineOrdersTool::DESCRIPTION,
+ inputSchema: AnalyzeOfflineOrdersTool::inputSchema(),
+ )
+ ->addTool(
+ handler: $this->merchantRankingHandler(),
+ name: MerchantRankingTool::NAME,
+ description: MerchantRankingTool::DESCRIPTION,
+ inputSchema: MerchantRankingTool::inputSchema(),
+ );
+ }
+
+ private function orderSummaryHandler(): \Closure
+ {
+ return fn (
+ ?string $date = null,
+ ?int $status = null,
+ ?string $keyword = null,
+ ?string $fieldKey = null,
+ ?string $payType = null,
+ ?string $type = null,
+ ?int $btcId = null,
+ ): array => ($this->orderSummaryTool)($date, $status, $keyword, $fieldKey, $payType, $type, $btcId);
+ }
+
+ private function analyzeOfflineOrdersHandler(): \Closure
+ {
+ return fn (
+ ?string $date = null,
+ ?int $status = null,
+ ?string $keyword = null,
+ ?string $fieldKey = null,
+ ?string $payType = null,
+ ?string $type = null,
+ ?int $btcId = null,
+ ): array => ($this->analyzeOfflineOrdersTool)($date, $status, $keyword, $fieldKey, $payType, $type, $btcId);
+ }
+
+ private function merchantRankingHandler(): \Closure
+ {
+ return fn (
+ ?string $date = null,
+ ?int $status = null,
+ ?string $keyword = null,
+ ?string $fieldKey = null,
+ ?string $payType = null,
+ ?string $type = null,
+ ?int $btcId = null,
+ ?string $sortBy = null,
+ ?int $limit = null,
+ ): array => ($this->merchantRankingTool)($date, $status, $keyword, $fieldKey, $payType, $type, $btcId, $sortBy, $limit);
+ }
+}