mirror of https://github.com/langgenius/dify.git
Merge 1fd382c933 into 49a1fae555
This commit is contained in:
commit
0e6834b425
|
|
@ -3,6 +3,10 @@ __pycache__/
|
|||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# AgentPay MCP local debug artifacts
|
||||
plugins/community/agentpay-mcp-payments/.tmp/
|
||||
plugins/community/agentpay-mcp-payments/*.local.md
|
||||
plugins/community/agentpay-mcp-payments/*.local.log
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,12 @@ SUGGESTED_QUESTIONS_TEMPERATURE=0.3
|
|||
|
||||
See the [Suggested Questions Configuration Guide](docs/suggested-questions-configuration.md) for detailed examples and usage instructions.
|
||||
|
||||
#### External MCP Payment Integration (AgentPay Example)
|
||||
|
||||
If you want your agents to execute payment-capable workflows, follow the
|
||||
[AgentPay MCP Integration Guide](docs/agentpay-mcp-integration.md).
|
||||
The guide is beginner-friendly and uses Dify's existing MCP provider flow.
|
||||
|
||||
### Metrics Monitoring with Grafana
|
||||
|
||||
Import the dashboard to Grafana, using Dify's PostgreSQL database as data source, to monitor metrics in granularity of apps, tenants, messages, and more.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
# AgentPay MCP Payments Integration Guide
|
||||
|
||||
This guide explains how to connect and use the AgentPay MCP Payments plugin with Dify, enabling agents and workflows to discover, fund, and invoke paid tools securely.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
- Dify instance (v1.13.0+ recommended)
|
||||
- AgentPay MCP server (HTTP or stdio)
|
||||
- AgentPay Gateway Key (starts with `apg_...`)
|
||||
- (Optional) Node.js for local stdio mode
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 2. Get Your AgentPay Gateway Key
|
||||
|
||||
Register for a gateway key:
|
||||
|
||||
```bash
|
||||
curl -X POST https://agentpay.metaltorque.dev/gateway/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "you@example.com"}'
|
||||
```
|
||||
|
||||
Save the returned key (format: `apg_...`).
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 3. Start the AgentPay MCP Server
|
||||
|
||||
- **Recommended (HTTP):**
|
||||
```bash
|
||||
npx -y supergateway --stdio "npx -y mcp-server-agentpay" --port 8000
|
||||
# HTTP endpoints: http://localhost:8000/message (POST), http://localhost:8000/sse (SSE)
|
||||
```
|
||||
- **Direct stdio (fallback):**
|
||||
```bash
|
||||
npx -y mcp-server-agentpay
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 4. Add MCP Server in Dify
|
||||
|
||||
1. Go to **Tools → MCP → Add MCP Server (HTTP)**
|
||||
1. Fill in:
|
||||
- **Server URL:** `http://localhost:8000/message` (or your HTTP endpoint)
|
||||
- **Name:** `AgentPay`
|
||||
- **Server Identifier:** `agentpay`
|
||||
- **Headers:** Add `X-AgentPay-Gateway-Key: apg_...`
|
||||
- **Dynamic Client Registration:** OFF
|
||||
- Leave Client ID/Secret blank
|
||||
1. Save and authorize.
|
||||
1. Click **Fetch Tools** to load available tools.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 5. Attach Tools to Agents/Workflows
|
||||
|
||||
- Enable tools like `discover_tools`, `check_balance`, `call_tool` in your workflow or agent config.
|
||||
- Example prompts:
|
||||
- "Search for web scraping tools."
|
||||
- "Check my AgentPay balance."
|
||||
- "Call a paid tool with arguments: { ... }"
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 6. Troubleshooting
|
||||
|
||||
- **No tools appear:**
|
||||
- Check MCP server logs for errors
|
||||
- Ensure gateway key is correct and in headers
|
||||
- Try both `/message` and `/sse` endpoints for Server URL
|
||||
- **Tool call fails:**
|
||||
- Check Dify plugin logs for error details
|
||||
- Confirm MCP server is running and reachable
|
||||
- Test with curl (see below)
|
||||
- **Test MCP directly:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/message \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-AgentPay-Gateway-Key: apg_..." \
|
||||
-d '{"tool":"check_balance","args":{}}'
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 7. Security & Best Practices
|
||||
|
||||
- Never commit gateway keys to source control
|
||||
- Rotate keys if exposed
|
||||
- Restrict tool usage in production
|
||||
- All payments are routed via AgentPay gateway; no private key custody
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 8. Support
|
||||
|
||||
- For advanced troubleshooting, see the operator runbook or contact plugin maintainers.
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
# AgentPay Payment Layer (Dify Tool Plugin)
|
||||
|
||||
Community tool plugin for AgentPay MCP operations in Dify workflows.
|
||||
|
||||
## Scope
|
||||
|
||||
This plugin is intended for tool execution in workflows (direct tool nodes).
|
||||
|
||||
Provided tools:
|
||||
|
||||
- `discover_tools`
|
||||
- `list_tools`
|
||||
- `check_balance`
|
||||
- `fund_wallet_stripe`
|
||||
- `call_tool`
|
||||
- `get_usage`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Dify running and plugin daemon healthy
|
||||
- Valid AgentPay gateway key (`apg_...`)
|
||||
- Reachable MCP endpoint
|
||||
|
||||
Register key:
|
||||
|
||||
```bash
|
||||
curl -X POST https://agentpay.metaltorque.dev/gateway/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"you@example.com"}'
|
||||
```
|
||||
|
||||
## MCP Runtime Topology
|
||||
|
||||
Use one topology only during a test run.
|
||||
|
||||
### Option A: Host runtime (recommended for local source-mode API)
|
||||
|
||||
Run on host:
|
||||
|
||||
```bash
|
||||
npx -y supergateway@3.4.3 --stdio "npx -y mcp-server-agentpay" --port 8000 --outputTransport streamableHttp
|
||||
```
|
||||
|
||||
Set MCP URL in Dify:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8000/mcp
|
||||
```
|
||||
|
||||
### Option B: Docker runtime
|
||||
|
||||
Run in Docker with published port:
|
||||
|
||||
```bash
|
||||
docker run --name agentpay-supergateway --rm -d -p 8000:8000 node:20-alpine sh -c "npx -y supergateway@3.4.3 --stdio 'npx -y mcp-server-agentpay' --port 8000 --outputTransport streamableHttp"
|
||||
```
|
||||
|
||||
Set MCP URL in Dify:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8000/mcp
|
||||
```
|
||||
|
||||
## Dify Provider Configuration
|
||||
|
||||
Required credential:
|
||||
|
||||
- `AgentPay Gateway Key`
|
||||
|
||||
Optional credential overrides:
|
||||
|
||||
- `AgentPay Gateway URL`
|
||||
- `AgentPay MCP HTTP URL`
|
||||
- `Launch mode` (`auto`, `http`, `stdio`)
|
||||
- `stdio command` (default `npx`)
|
||||
|
||||
## Workflow Wiring (Direct Tool Execution)
|
||||
|
||||
Minimal flow:
|
||||
|
||||
1. `Start`
|
||||
1. `check_balance` tool node
|
||||
1. `Answer` node
|
||||
|
||||
Answer template:
|
||||
|
||||
```text
|
||||
{{#<check_balance_node_id>.text#}}
|
||||
```
|
||||
|
||||
Insert variable from picker (`CHECK_BALANCE -> text`) so node ID stays correct.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Success requires both:
|
||||
|
||||
1. Tool node last run status = `SUCCESS`
|
||||
1. Supergateway log shows `"method":"tools/call"` with `"name":"check_balance"`
|
||||
|
||||
If you only see `initialize` and `tools/list`, execution did not reach tool invoke path.
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Tool timeout
|
||||
|
||||
Cause: plugin daemon cannot reach MCP URL.
|
||||
|
||||
Fix:
|
||||
|
||||
- ensure MCP URL matches active topology
|
||||
- avoid mixed host/container URLs in the same test session
|
||||
|
||||
### Empty answer
|
||||
|
||||
Cause: wrong output mapping.
|
||||
|
||||
Fix:
|
||||
|
||||
- map `CHECK_BALANCE -> text` in `Answer`
|
||||
|
||||
### Accept header error in manual tests
|
||||
|
||||
Manual probes against streamable HTTP must send:
|
||||
|
||||
- `Accept: application/json, text/event-stream`
|
||||
|
||||
This does not affect Dify normal MCP calls.
|
||||
|
||||
## Contribution Notes
|
||||
|
||||
Aligned with Dify contribution guidance:
|
||||
|
||||
- keep changes scoped and minimal
|
||||
- include reproducible logs for bug fixes
|
||||
- link an issue in PR description
|
||||
- include screenshots for workflow/tool verification
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="128" height="128" rx="24" fill="#0F172A"/>
|
||||
<path d="M34 64C34 47.4315 47.4315 34 64 34H94V50H64C56.268 50 50 56.268 50 64C50 71.732 56.268 78 64 78H70V62H86V94H64C47.4315 94 34 80.5685 34 64Z" fill="#38BDF8"/>
|
||||
<circle cx="92" cy="42" r="10" fill="#22C55E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 384 B |
|
|
@ -0,0 +1,23 @@
|
|||
# pyright: reportMissingImports=false
|
||||
|
||||
from typing import Any
|
||||
|
||||
from dify_plugin import ToolProvider
|
||||
from dify_plugin.errors.tool import ToolProviderCredentialValidationError
|
||||
|
||||
from tools.client import AgentPayMCPClient, AgentPayMCPError
|
||||
|
||||
|
||||
class AgentPayMCPPaymentsProvider(ToolProvider):
|
||||
"""Credential validation and provider-level lifecycle for AgentPay MCP tools."""
|
||||
|
||||
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
|
||||
try:
|
||||
client = AgentPayMCPClient.from_credentials(credentials)
|
||||
client.call_tool("check_balance", {})
|
||||
except AgentPayMCPError as exc:
|
||||
raise ToolProviderCredentialValidationError(str(exc)) from exc
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise ToolProviderCredentialValidationError(
|
||||
f"Failed to validate AgentPay credentials: {exc}"
|
||||
) from exc
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# pyright: reportMissingImports=false
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from dify_plugin import Plugin
|
||||
|
||||
|
||||
class AgentPayMCPPaymentsPlugin(Plugin):
|
||||
"""Plugin entrypoint for AgentPay MCP Payments."""
|
||||
|
||||
def _invoke(self, payload: Mapping[str, object]) -> Mapping[str, object]:
|
||||
return payload
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
AgentPayMCPPaymentsPlugin().run()
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
version: 0.0.1
|
||||
type: plugin
|
||||
author: AI Agent Economy Community
|
||||
name: agentpay_mcp_payments
|
||||
label:
|
||||
en_US: AgentPay MCP Payments
|
||||
zh_Hans: AgentPay MCP 支付层
|
||||
description:
|
||||
en_US: MCP-based payment layer for discovering and invoking metered tools through AgentPay.
|
||||
created_at: "2026-03-14T12:00:00Z"
|
||||
icon: _assets/icon.svg
|
||||
resource:
|
||||
memory: 268435456
|
||||
permission:
|
||||
tool:
|
||||
enabled: true
|
||||
app:
|
||||
enabled: true
|
||||
storage:
|
||||
enabled: false
|
||||
plugins:
|
||||
tools:
|
||||
- provider/agentpay_mcp_payments.yaml
|
||||
meta:
|
||||
version: 0.0.1
|
||||
arch:
|
||||
- amd64
|
||||
- arm64
|
||||
runner:
|
||||
language: python
|
||||
version: "3.12"
|
||||
entrypoint: main
|
||||
privacy: ./privacy.md
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Privacy Policy
|
||||
|
||||
This plugin sends user-supplied tool parameters and credential-backed requests to AgentPay endpoints for payment operations.
|
||||
|
||||
## Data processed
|
||||
|
||||
- provider credentials configured by workspace admins
|
||||
- tool input parameters passed at runtime
|
||||
- response payloads returned by AgentPay MCP server/gateway
|
||||
|
||||
## Data retention
|
||||
|
||||
This plugin does not intentionally persist user data beyond runtime process memory.
|
||||
|
||||
## Security notes
|
||||
|
||||
- Never commit gateway keys into source control.
|
||||
- Rotate keys if exposed.
|
||||
- Restrict tool usage in production with least-privilege principles.
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
identity:
|
||||
author: AI Agent Economy Community
|
||||
name: agentpay_mcp_payments
|
||||
label:
|
||||
en_US: AgentPay Payment Layer
|
||||
zh_Hans: AgentPay 支付层
|
||||
description:
|
||||
en_US: Discover and invoke paid MCP tools with AgentPay metering and funding flows.
|
||||
zh_Hans: 使用 AgentPay 计费能力发现并调用付费 MCP 工具。
|
||||
icon: _assets/icon.svg
|
||||
tags:
|
||||
- finance
|
||||
- business
|
||||
- utilities
|
||||
|
||||
credentials_for_provider:
|
||||
agentpay_gateway_key:
|
||||
type: secret-input
|
||||
required: true
|
||||
label:
|
||||
en_US: AgentPay Gateway Key
|
||||
zh_Hans: AgentPay 网关密钥
|
||||
placeholder:
|
||||
en_US: apg_xxx
|
||||
zh_Hans: 输入 apg_ 开头的密钥
|
||||
help:
|
||||
en_US: Register once to create a wallet and key.
|
||||
zh_Hans: 先注册获取钱包和网关密钥。
|
||||
url: https://agentpay.metaltorque.dev/docs
|
||||
agentpay_url:
|
||||
type: text-input
|
||||
required: false
|
||||
label:
|
||||
en_US: AgentPay Gateway URL
|
||||
zh_Hans: AgentPay 网关地址
|
||||
placeholder:
|
||||
en_US: https://agentpay.metaltorque.dev
|
||||
zh_Hans: https://agentpay.metaltorque.dev
|
||||
agentpay_mcp_http_url:
|
||||
type: text-input
|
||||
required: false
|
||||
label:
|
||||
en_US: AgentPay MCP HTTP URL (optional)
|
||||
zh_Hans: AgentPay MCP HTTP 地址(可选)
|
||||
placeholder:
|
||||
en_US: https://your-agentpay-host/mcp
|
||||
zh_Hans: https://your-agentpay-host/mcp
|
||||
agentpay_launch_mode:
|
||||
type: select
|
||||
required: false
|
||||
label:
|
||||
en_US: Launch Mode
|
||||
zh_Hans: 启动模式
|
||||
options:
|
||||
- value: auto
|
||||
label:
|
||||
en_US: auto (HTTP first, stdio fallback)
|
||||
zh_Hans: 自动(优先 HTTP,失败回退 stdio)
|
||||
- value: http
|
||||
label:
|
||||
en_US: HTTP only
|
||||
zh_Hans: 仅 HTTP
|
||||
- value: stdio
|
||||
label:
|
||||
en_US: stdio only
|
||||
zh_Hans: 仅 stdio
|
||||
default: auto
|
||||
agentpay_command:
|
||||
type: text-input
|
||||
required: false
|
||||
label:
|
||||
en_US: stdio command
|
||||
zh_Hans: stdio 命令
|
||||
placeholder:
|
||||
en_US: npx
|
||||
zh_Hans: npx
|
||||
|
||||
tools:
|
||||
- tools/discover_tools.yaml
|
||||
- tools/list_tools.yaml
|
||||
- tools/check_balance.yaml
|
||||
- tools/fund_wallet_stripe.yaml
|
||||
- tools/call_tool.yaml
|
||||
- tools/get_usage.yaml
|
||||
|
||||
extra:
|
||||
python:
|
||||
source: agentpay_mcp_payments.py
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import sys
|
||||
import os
|
||||
import pytest
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
from tools.client import AgentPayAuthError, AgentPayMCPClient
|
||||
|
||||
|
||||
def test_requires_gateway_key() -> None:
|
||||
try:
|
||||
AgentPayMCPClient.from_credentials({})
|
||||
raise AssertionError("Expected missing key error")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
assert "gateway key" in str(exc).lower()
|
||||
|
||||
|
||||
def test_default_credential_values() -> None:
|
||||
client = AgentPayMCPClient.from_credentials({"agentpay_gateway_key": "apg_test"})
|
||||
assert client.gateway_key == "apg_test"
|
||||
assert client.gateway_url == "https://agentpay.metaltorque.dev"
|
||||
assert client.launch_mode == "auto"
|
||||
|
||||
|
||||
def test_auto_mode_raises_auth_error_without_stdio_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
client = AgentPayMCPClient.from_credentials({"agentpay_gateway_key": "apg_test"})
|
||||
|
||||
stdio_called = False
|
||||
|
||||
def mock_http_call(self: AgentPayMCPClient, tool_name: str, arguments: dict[str, object]) -> dict[str, object]:
|
||||
raise AgentPayAuthError("invalid key")
|
||||
|
||||
def mock_stdio_call(self: AgentPayMCPClient, tool_name: str, arguments: dict[str, object]) -> dict[str, object]:
|
||||
nonlocal stdio_called
|
||||
stdio_called = True
|
||||
return {"ok": True}
|
||||
|
||||
monkeypatch.setattr(AgentPayMCPClient, "_http_call", mock_http_call)
|
||||
monkeypatch.setattr(AgentPayMCPClient, "_stdio_call", mock_stdio_call)
|
||||
|
||||
with pytest.raises(AgentPayAuthError):
|
||||
client.call_tool("check_balance", {})
|
||||
|
||||
assert stdio_called is False
|
||||
|
||||
|
||||
def test_auto_mode_falls_back_to_stdio_for_non_auth_errors(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
client = AgentPayMCPClient.from_credentials({"agentpay_gateway_key": "apg_test"})
|
||||
|
||||
def mock_http_call(self: AgentPayMCPClient, tool_name: str, arguments: dict[str, object]) -> dict[str, object]:
|
||||
raise RuntimeError("network issue")
|
||||
|
||||
def mock_stdio_call(self: AgentPayMCPClient, tool_name: str, arguments: dict[str, object]) -> dict[str, object]:
|
||||
return {"ok": True}
|
||||
|
||||
monkeypatch.setattr(AgentPayMCPClient, "_http_call", mock_http_call)
|
||||
monkeypatch.setattr(AgentPayMCPClient, "_stdio_call", mock_stdio_call)
|
||||
|
||||
assert client.call_tool("check_balance", {}) == {"ok": True}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# pyright: reportMissingImports=false
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
from dify_plugin import Tool
|
||||
from dify_plugin.entities.tool import ToolInvokeMessage
|
||||
|
||||
from tools.client import AgentPayMCPClient
|
||||
from tools.common import parse_json_object
|
||||
|
||||
|
||||
class CallToolTool(Tool):
|
||||
def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]:
|
||||
tool_name = str(tool_parameters.get("tool_name") or "").strip()
|
||||
if not tool_name:
|
||||
raise ValueError("tool_name is required")
|
||||
|
||||
arguments = parse_json_object(
|
||||
tool_parameters.get("arguments_json"),
|
||||
field_name="arguments_json",
|
||||
)
|
||||
|
||||
client = AgentPayMCPClient.from_credentials(self.runtime.credentials)
|
||||
result = client.call_tool("call_tool", {"tool": tool_name, "arguments": arguments})
|
||||
yield self.create_json_message(result)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
identity:
|
||||
name: call_tool
|
||||
author: AI Agent Economy Community
|
||||
label:
|
||||
en_US: Paid Tool Call
|
||||
description:
|
||||
human:
|
||||
en_US: Invoke a paid tool through AgentPay metering.
|
||||
llm: Call a third-party paid tool by name with JSON arguments. AgentPay auto-provisions and meters usage.
|
||||
parameters:
|
||||
- name: tool_name
|
||||
type: string
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Tool Name
|
||||
human_description:
|
||||
en_US: Registered tool name in AgentPay marketplace.
|
||||
llm_description: Name of the paid tool to call.
|
||||
- name: arguments_json
|
||||
type: string
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Arguments JSON
|
||||
human_description:
|
||||
en_US: JSON object string for tool input arguments.
|
||||
llm_description: JSON string object for the tool arguments.
|
||||
placeholder:
|
||||
en_US: '{"url":"https://example.com"}'
|
||||
extra:
|
||||
python:
|
||||
source: tools/call_tool.py
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# pyright: reportMissingImports=false
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
from dify_plugin import Tool
|
||||
from dify_plugin.entities.tool import ToolInvokeMessage
|
||||
|
||||
from tools.client import AgentPayMCPClient
|
||||
|
||||
|
||||
class CheckBalanceTool(Tool):
|
||||
def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]:
|
||||
_ = tool_parameters
|
||||
client = AgentPayMCPClient.from_credentials(self.runtime.credentials)
|
||||
# Always pass the gatewayKey in arguments for MCP compatibility
|
||||
result = client.call_tool("check_balance", {"gatewayKey": client.gateway_key})
|
||||
yield self.create_json_message(result)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
identity:
|
||||
name: check_balance
|
||||
author: AI Agent Economy Community
|
||||
label:
|
||||
en_US: Check Balance
|
||||
description:
|
||||
human:
|
||||
en_US: Check wallet balance and provisioned tools.
|
||||
llm: Check AgentPay wallet credits and provisioned tool access.
|
||||
parameters: []
|
||||
extra:
|
||||
python:
|
||||
source: tools/check_balance.py
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
class AgentPayMCPError(RuntimeError):
|
||||
"""Base error for AgentPay MCP plugin client."""
|
||||
|
||||
|
||||
class AgentPayAuthError(AgentPayMCPError):
|
||||
"""Raised when credentials are invalid."""
|
||||
|
||||
|
||||
class AgentPayTimeoutError(AgentPayMCPError):
|
||||
"""Raised when request exceeds timeout."""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AgentPayMCPClient:
|
||||
gateway_key: str
|
||||
gateway_url: str
|
||||
mcp_http_url: str | None
|
||||
launch_mode: str
|
||||
command: str
|
||||
timeout_seconds: int = 30
|
||||
|
||||
@classmethod
|
||||
def from_credentials(cls, credentials: dict[str, Any]) -> "AgentPayMCPClient":
|
||||
gateway_key = str(credentials.get("agentpay_gateway_key") or "").strip()
|
||||
if not gateway_key:
|
||||
raise AgentPayAuthError("Missing AgentPay gateway key")
|
||||
|
||||
gateway_url = str(credentials.get("agentpay_url") or "https://agentpay.metaltorque.dev").strip()
|
||||
mcp_http_url = str(credentials.get("agentpay_mcp_http_url") or "").strip() or None
|
||||
launch_mode = str(credentials.get("agentpay_launch_mode") or "auto").strip().lower()
|
||||
command = str(credentials.get("agentpay_command") or "npx").strip()
|
||||
|
||||
return cls(
|
||||
gateway_key=gateway_key,
|
||||
gateway_url=gateway_url,
|
||||
mcp_http_url=mcp_http_url,
|
||||
launch_mode=launch_mode,
|
||||
command=command,
|
||||
)
|
||||
|
||||
def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
if self.launch_mode == "http":
|
||||
return self._http_call(tool_name, arguments)
|
||||
|
||||
if self.launch_mode == "stdio":
|
||||
return self._stdio_call(tool_name, arguments)
|
||||
|
||||
# auto mode: prefer HTTP, fallback to stdio.
|
||||
try:
|
||||
return self._http_call(tool_name, arguments)
|
||||
except AgentPayAuthError:
|
||||
raise
|
||||
except Exception:
|
||||
return self._stdio_call(tool_name, arguments)
|
||||
|
||||
def _http_call(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
print(f"[AgentPayMCPClient] HTTP call to {self.mcp_http_url} with tool '{tool_name}' and arguments {arguments}")
|
||||
if not self.mcp_http_url:
|
||||
raise AgentPayMCPError("MCP HTTP URL is not configured")
|
||||
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "dify-agentpay-1",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": tool_name,
|
||||
"arguments": arguments,
|
||||
},
|
||||
}
|
||||
|
||||
request = urllib.request.Request(
|
||||
self.mcp_http_url,
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.gateway_key}",
|
||||
"X-AgentPay-Gateway-Key": self.gateway_key,
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
|
||||
body = response.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code in (401, 403):
|
||||
raise AgentPayAuthError("AgentPay authorization failed") from exc
|
||||
raise AgentPayMCPError(f"AgentPay HTTP call failed: {exc}") from exc
|
||||
except TimeoutError as exc:
|
||||
raise AgentPayTimeoutError("AgentPay HTTP call timed out") from exc
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise AgentPayMCPError(f"AgentPay HTTP call failed: {exc}") from exc
|
||||
|
||||
try:
|
||||
parsed = json.loads(body)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise AgentPayMCPError("AgentPay HTTP response is not valid JSON") from exc
|
||||
|
||||
return self._normalize_mcp_result(parsed)
|
||||
|
||||
def _stdio_call(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
env = dict(os.environ)
|
||||
env["AGENTPAY_GATEWAY_KEY"] = self.gateway_key
|
||||
env.setdefault("AGENTPAY_URL", self.gateway_url)
|
||||
|
||||
args = [self.command, "-y", "mcp-server-agentpay"]
|
||||
process = subprocess.Popen(
|
||||
args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=env,
|
||||
)
|
||||
|
||||
try:
|
||||
initialize_request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {},
|
||||
"clientInfo": {
|
||||
"name": "dify-agentpay-plugin",
|
||||
"version": "0.0.1",
|
||||
},
|
||||
},
|
||||
}
|
||||
initialized_notification = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/initialized",
|
||||
"params": {},
|
||||
}
|
||||
tool_call_request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": tool_name,
|
||||
"arguments": arguments,
|
||||
},
|
||||
}
|
||||
|
||||
self._write_message(process, initialize_request)
|
||||
_ = self._read_message(process)
|
||||
self._write_message(process, initialized_notification)
|
||||
self._write_message(process, tool_call_request)
|
||||
response = self._read_message(process)
|
||||
|
||||
return self._normalize_mcp_result(response)
|
||||
except TimeoutError as exc:
|
||||
raise AgentPayTimeoutError("AgentPay stdio call timed out") from exc
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise AgentPayMCPError(f"AgentPay stdio call failed: {exc}") from exc
|
||||
finally:
|
||||
process.kill()
|
||||
|
||||
def _write_message(self, process: subprocess.Popen[bytes], payload: dict[str, Any]) -> None:
|
||||
if process.stdin is None:
|
||||
raise AgentPayMCPError("stdio input is unavailable")
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
header = f"Content-Length: {len(data)}\r\n\r\n".encode("utf-8")
|
||||
process.stdin.write(header + data)
|
||||
process.stdin.flush()
|
||||
|
||||
def _read_message(self, process: subprocess.Popen[bytes]) -> dict[str, Any]:
|
||||
if process.stdout is None:
|
||||
raise AgentPayMCPError("stdio output is unavailable")
|
||||
|
||||
deadline = time.time() + self.timeout_seconds
|
||||
|
||||
def read_line() -> bytes:
|
||||
while time.time() < deadline:
|
||||
line = process.stdout.readline()
|
||||
if line:
|
||||
return line
|
||||
raise TimeoutError("Timed out reading MCP headers")
|
||||
|
||||
content_length = 0
|
||||
while True:
|
||||
line = read_line()
|
||||
if line in (b"\r\n", b"\n", b""):
|
||||
break
|
||||
lower = line.decode("utf-8").lower()
|
||||
if lower.startswith("content-length:"):
|
||||
content_length = int(lower.split(":", 1)[1].strip())
|
||||
|
||||
if content_length <= 0:
|
||||
raise AgentPayMCPError("Invalid MCP content length")
|
||||
|
||||
remaining = content_length
|
||||
chunks: list[bytes] = []
|
||||
while remaining > 0:
|
||||
if time.time() >= deadline:
|
||||
raise TimeoutError("Timed out reading MCP payload")
|
||||
chunk = process.stdout.read(remaining)
|
||||
if not chunk:
|
||||
raise AgentPayMCPError("MCP server closed output early")
|
||||
chunks.append(chunk)
|
||||
remaining -= len(chunk)
|
||||
|
||||
data = b"".join(chunks)
|
||||
return json.loads(data.decode("utf-8"))
|
||||
|
||||
def _normalize_mcp_result(self, response: dict[str, Any]) -> dict[str, Any]:
|
||||
if "error" in response:
|
||||
message = response["error"].get("message", "Unknown MCP error")
|
||||
if "auth" in message.lower() or "key" in message.lower():
|
||||
raise AgentPayAuthError(message)
|
||||
raise AgentPayMCPError(message)
|
||||
|
||||
result = response.get("result")
|
||||
if not isinstance(result, dict):
|
||||
raise AgentPayMCPError("Invalid MCP result payload")
|
||||
|
||||
content = result.get("content")
|
||||
if isinstance(content, list):
|
||||
for item in content:
|
||||
if item.get("type") == "text" and "text" in item:
|
||||
text = item["text"]
|
||||
try:
|
||||
parsed_text = json.loads(text)
|
||||
if isinstance(parsed_text, dict):
|
||||
return parsed_text
|
||||
except json.JSONDecodeError:
|
||||
return {"text": text}
|
||||
if item.get("type") == "json" and isinstance(item.get("json"), dict):
|
||||
return item["json"]
|
||||
|
||||
return result
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
def parse_json_object(value: str | None, *, field_name: str) -> dict[str, Any]:
|
||||
if not value:
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(value)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"{field_name} must be valid JSON") from exc
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"{field_name} must be a JSON object")
|
||||
return data
|
||||
|
||||
|
||||
def to_int(value: Any, *, default: int) -> int:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return int(value)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# pyright: reportMissingImports=false
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
from dify_plugin import Tool
|
||||
from dify_plugin.entities.tool import ToolInvokeMessage
|
||||
|
||||
from tools.client import AgentPayMCPClient
|
||||
from tools.common import to_int
|
||||
|
||||
|
||||
class DiscoverToolsTool(Tool):
|
||||
def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]:
|
||||
client = AgentPayMCPClient.from_credentials(self.runtime.credentials)
|
||||
payload = {
|
||||
"query": tool_parameters.get("query", ""),
|
||||
"limit": to_int(tool_parameters.get("limit"), default=10),
|
||||
}
|
||||
result = client.call_tool("discover_tools", payload)
|
||||
yield self.create_json_message(result)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
identity:
|
||||
name: discover_tools
|
||||
author: AI Agent Economy Community
|
||||
label:
|
||||
en_US: Discover Tools
|
||||
description:
|
||||
human:
|
||||
en_US: Search available paid tools by keyword.
|
||||
llm: Search available paid tools by keyword and return pricing metadata.
|
||||
parameters:
|
||||
- name: query
|
||||
type: string
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Query
|
||||
human_description:
|
||||
en_US: Search keyword such as security, seo, or scraping.
|
||||
llm_description: Keyword to search in tool marketplace.
|
||||
- name: limit
|
||||
type: number
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Limit
|
||||
human_description:
|
||||
en_US: Max number of results to return.
|
||||
llm_description: Maximum number of results to return.
|
||||
default: 10
|
||||
extra:
|
||||
python:
|
||||
source: tools/discover_tools.py
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# pyright: reportMissingImports=false
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
from dify_plugin import Tool
|
||||
from dify_plugin.entities.tool import ToolInvokeMessage
|
||||
|
||||
from tools.client import AgentPayMCPClient
|
||||
|
||||
|
||||
class FundWalletStripeTool(Tool):
|
||||
def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]:
|
||||
client = AgentPayMCPClient.from_credentials(self.runtime.credentials)
|
||||
payload = {
|
||||
"package": str(tool_parameters.get("package") or "micro"),
|
||||
}
|
||||
result = client.call_tool("fund_wallet_stripe", payload)
|
||||
yield self.create_json_message(result)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
identity:
|
||||
name: fund_wallet_stripe
|
||||
author: AI Agent Economy Community
|
||||
label:
|
||||
en_US: Fund Wallet (Stripe)
|
||||
description:
|
||||
human:
|
||||
en_US: Generate a Stripe checkout URL to fund wallet credits.
|
||||
llm: Generate a Stripe checkout URL for adding credits to AgentPay wallet.
|
||||
parameters:
|
||||
- name: package
|
||||
type: select
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Package
|
||||
human_description:
|
||||
en_US: Credit package to purchase.
|
||||
llm_description: Credit package name.
|
||||
default: micro
|
||||
options:
|
||||
- value: micro
|
||||
label:
|
||||
en_US: micro
|
||||
- value: small
|
||||
label:
|
||||
en_US: small
|
||||
- value: medium
|
||||
label:
|
||||
en_US: medium
|
||||
- value: large
|
||||
label:
|
||||
en_US: large
|
||||
- value: whale
|
||||
label:
|
||||
en_US: whale
|
||||
extra:
|
||||
python:
|
||||
source: tools/fund_wallet_stripe.py
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# pyright: reportMissingImports=false
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
from dify_plugin import Tool
|
||||
from dify_plugin.entities.tool import ToolInvokeMessage
|
||||
|
||||
from tools.client import AgentPayMCPClient
|
||||
from tools.common import to_int
|
||||
|
||||
|
||||
class GetUsageTool(Tool):
|
||||
def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]:
|
||||
client = AgentPayMCPClient.from_credentials(self.runtime.credentials)
|
||||
payload = {
|
||||
"limit": to_int(tool_parameters.get("limit"), default=20),
|
||||
}
|
||||
result = client.call_tool("get_usage", payload)
|
||||
yield self.create_json_message(result)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
identity:
|
||||
name: get_usage
|
||||
author: AI Agent Economy Community
|
||||
label:
|
||||
en_US: Get Usage
|
||||
description:
|
||||
human:
|
||||
en_US: View recent metered calls and costs.
|
||||
llm: Get recent usage records and spending details.
|
||||
parameters:
|
||||
- name: limit
|
||||
type: number
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Limit
|
||||
human_description:
|
||||
en_US: Number of recent usage rows.
|
||||
llm_description: Number of recent usage entries to fetch.
|
||||
default: 20
|
||||
extra:
|
||||
python:
|
||||
source: tools/get_usage.py
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# pyright: reportMissingImports=false
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
from dify_plugin import Tool
|
||||
from dify_plugin.entities.tool import ToolInvokeMessage
|
||||
|
||||
from tools.client import AgentPayMCPClient
|
||||
from tools.common import to_int
|
||||
|
||||
|
||||
class ListToolsTool(Tool):
|
||||
def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]:
|
||||
client = AgentPayMCPClient.from_credentials(self.runtime.credentials)
|
||||
payload = {
|
||||
"limit": to_int(tool_parameters.get("limit"), default=25),
|
||||
}
|
||||
result = client.call_tool("list_tools", payload)
|
||||
yield self.create_json_message(result)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
identity:
|
||||
name: list_tools
|
||||
author: AI Agent Economy Community
|
||||
label:
|
||||
en_US: List Tools
|
||||
description:
|
||||
human:
|
||||
en_US: List all available paid tools and their pricing.
|
||||
llm: List all available paid tools and pricing plans.
|
||||
parameters:
|
||||
- name: limit
|
||||
type: number
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Limit
|
||||
human_description:
|
||||
en_US: Max number of tools to return.
|
||||
llm_description: Maximum number of tools to return.
|
||||
default: 25
|
||||
extra:
|
||||
python:
|
||||
source: tools/list_tools.py
|
||||
|
|
@ -348,6 +348,87 @@ describe('useMCPModalForm', () => {
|
|||
})
|
||||
|
||||
describe('handleUrlBlur', () => {
|
||||
it('should safely continue when AgentPay URL detection throws', async () => {
|
||||
const { uploadRemoteFileInfo } = await import('@/service/common')
|
||||
vi.mocked(uploadRemoteFileInfo).mockResolvedValueOnce({
|
||||
id: 'file123',
|
||||
name: 'icon.png',
|
||||
size: 1024,
|
||||
mime_type: 'image/png',
|
||||
url: 'https://example.com/files/file123/file-preview/icon.png',
|
||||
} as unknown as { id: string, name: string, size: number, mime_type: string, url: string })
|
||||
|
||||
const OriginalURL = globalThis.URL
|
||||
let urlConstructorCalls = 0
|
||||
|
||||
class MockURL extends OriginalURL {
|
||||
constructor(url: string | URL, base?: string | URL) {
|
||||
urlConstructorCalls += 1
|
||||
if (urlConstructorCalls === 2)
|
||||
throw new Error('forced URL parse error')
|
||||
super(url, base)
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal('URL', MockURL)
|
||||
|
||||
const { result } = renderHook(() => useMCPModalForm())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.actions.handleUrlBlur('https://example.com/mcp')
|
||||
})
|
||||
|
||||
expect(result.current.state.name).toBe('')
|
||||
expect(result.current.state.serverIdentifier).toBe('')
|
||||
expect(result.current.state.appIcon.type).toBe('image')
|
||||
expect(result.current.state.isFetchingIcon).toBe(false)
|
||||
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('should auto-fill AgentPay defaults when url matches and fields are empty', async () => {
|
||||
vi.mocked(await import('@/service/common').then(m => m.uploadRemoteFileInfo)).mockResolvedValueOnce({
|
||||
id: 'file123',
|
||||
name: 'icon.png',
|
||||
size: 1024,
|
||||
mime_type: 'image/png',
|
||||
url: 'https://example.com/files/file123/file-preview/icon.png',
|
||||
} as unknown as { id: string, name: string, size: number, mime_type: string, url: string })
|
||||
|
||||
const { result } = renderHook(() => useMCPModalForm())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.actions.handleUrlBlur('https://api.agentpay.ai/mcp')
|
||||
})
|
||||
|
||||
expect(result.current.state.name).toBe('AgentPay')
|
||||
expect(result.current.state.serverIdentifier).toBe('agentpay')
|
||||
})
|
||||
|
||||
it('should not override existing name and server identifier when url matches AgentPay', async () => {
|
||||
vi.mocked(await import('@/service/common').then(m => m.uploadRemoteFileInfo)).mockResolvedValueOnce({
|
||||
id: 'file123',
|
||||
name: 'icon.png',
|
||||
size: 1024,
|
||||
mime_type: 'image/png',
|
||||
url: 'https://example.com/files/file123/file-preview/icon.png',
|
||||
} as unknown as { id: string, name: string, size: number, mime_type: string, url: string })
|
||||
|
||||
const { result } = renderHook(() => useMCPModalForm())
|
||||
|
||||
act(() => {
|
||||
result.current.actions.setName('Custom Name')
|
||||
result.current.actions.setServerIdentifier('custom-id')
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.actions.handleUrlBlur('https://api.agentpay.ai/mcp')
|
||||
})
|
||||
|
||||
expect(result.current.state.name).toBe('Custom Name')
|
||||
expect(result.current.state.serverIdentifier).toBe('custom-id')
|
||||
})
|
||||
|
||||
it('should not fetch icon in edit mode (when data is provided)', async () => {
|
||||
const mockData = {
|
||||
id: 'test',
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { MCPAuthMethod } from '@/app/components/tools/types'
|
|||
import { uploadRemoteFileInfo } from '@/service/common'
|
||||
|
||||
const DEFAULT_ICON = { type: 'emoji', icon: '🔗', background: '#6366F1' }
|
||||
const AGENTPAY_DEFAULT_NAME = 'AgentPay'
|
||||
const AGENTPAY_DEFAULT_SERVER_ID = 'agentpay'
|
||||
|
||||
const extractFileId = (url: string) => {
|
||||
const match = /files\/(.+?)\/file-preview/.exec(url)
|
||||
|
|
@ -32,6 +34,18 @@ const getInitialHeaders = (data?: ToolWithProvider): HeaderItem[] => {
|
|||
return Object.entries(data?.masked_headers || {}).map(([key, value]) => ({ id: uuid(), key, value }))
|
||||
}
|
||||
|
||||
const isAgentPayUrl = (urlValue: string) => {
|
||||
try {
|
||||
const parsed = new URL(urlValue)
|
||||
const host = parsed.hostname.toLowerCase()
|
||||
const path = parsed.pathname.toLowerCase()
|
||||
return host.includes('agentpay') || path.includes('agentpay')
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const isValidUrl = (string: string) => {
|
||||
try {
|
||||
const url = new URL(string)
|
||||
|
|
@ -112,11 +126,29 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
|
|||
const [clientID, setClientID] = useState(() => data?.authentication?.client_id || '')
|
||||
const [credentials, setCredentials] = useState(() => data?.authentication?.client_secret || '')
|
||||
|
||||
const applyAgentPayDefaults = useCallback((urlValue: string) => {
|
||||
if (!isAgentPayUrl(urlValue))
|
||||
return
|
||||
|
||||
// Set smart defaults only when fields are still empty.
|
||||
setName((prev) => {
|
||||
if (prev.trim())
|
||||
return prev
|
||||
return AGENTPAY_DEFAULT_NAME
|
||||
})
|
||||
setServerIdentifier((prev) => {
|
||||
if (prev.trim())
|
||||
return prev
|
||||
return AGENTPAY_DEFAULT_SERVER_ID
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleUrlBlur = useCallback(async (urlValue: string) => {
|
||||
if (data)
|
||||
return
|
||||
if (!isValidUrl(urlValue))
|
||||
return
|
||||
applyAgentPayDefaults(urlValue)
|
||||
const domain = getDomain(urlValue)
|
||||
const remoteIcon = `https://www.google.com/s2/favicons?domain=${domain}&sz=128`
|
||||
setIsFetchingIcon(true)
|
||||
|
|
@ -145,7 +177,7 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
|
|||
finally {
|
||||
setIsFetchingIcon(false)
|
||||
}
|
||||
}, [data])
|
||||
}, [applyAgentPayDefaults, data])
|
||||
|
||||
const resetIcon = useCallback(() => {
|
||||
setAppIcon(getIcon(data))
|
||||
|
|
|
|||
Loading…
Reference in New Issue