@agent-proxy-kit/puraroute-mcp-server 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +1 -0
- package/LICENSE +15 -0
- package/README.md +19 -2
- package/bundle/adapter-policy.json +11 -0
- package/bundle/brand.json +9 -0
- package/bundle/bundle-manifest.json +34 -0
- package/bundle/business-openapi.yaml +3518 -0
- package/bundle/run-mcp.mjs +38 -0
- package/bundle/setup.mjs +34 -0
- package/bundle/skill/puraroute-proxy/SKILL.md +26 -0
- package/bundle/skill/puraroute-proxy/examples/connection-cases.md +33 -0
- package/bundle/skill/puraroute-proxy/references/connection-formats.md +40 -0
- package/bundle/skill/puraroute-proxy/references/products.md +9 -0
- package/bundle/skill/puraroute-proxy/references/setup.md +19 -0
- package/bundle/skill/puraroute-proxy/references/troubleshooting.md +17 -0
- package/bundle/skill/puraroute-proxy/references/verified-contract.md +34 -0
- package/bundle/skill/puraroute-proxy/references/workflows.md +33 -0
- package/bundle/skill/puraroute-proxy/references/zh-CN.md +13 -0
- package/bundle/tool-constraints.json +28 -0
- package/dist/brand.js +40 -0
- package/dist/catalog/input-validator.js +40 -0
- package/dist/catalog/load.js +137 -0
- package/dist/catalog/operations.js +83 -0
- package/dist/catalog/project-schema.js +192 -0
- package/dist/cli.js +135 -0
- package/dist/config.js +32 -0
- package/dist/runtime/execute-operation.js +161 -0
- package/dist/runtime/register-openapi.js +12 -0
- package/dist/runtime/register-tools.js +4 -0
- package/dist/runtime/response.js +44 -0
- package/dist/runtime/secrets.js +5 -0
- package/dist/transports/stdio.js +14 -0
- package/package.json +33 -4
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const bundleDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
if (process.argv[2] === "--setup" && process.argv.length === 3) {
|
|
9
|
+
await import("./setup.mjs");
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
if (process.argv.length !== 2) {
|
|
13
|
+
process.stderr.write("Unknown MCP launcher argument.\n");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
let runtimeEntry;
|
|
17
|
+
const packagedRuntime = join(bundleDir, "..", "dist", "cli.js");
|
|
18
|
+
try {
|
|
19
|
+
await access(packagedRuntime);
|
|
20
|
+
runtimeEntry = packagedRuntime;
|
|
21
|
+
} catch {
|
|
22
|
+
try {
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
runtimeEntry = join(
|
|
25
|
+
dirname(require.resolve("proxy-agent-mcp-runtime/package.json")),
|
|
26
|
+
"dist/cli.js",
|
|
27
|
+
);
|
|
28
|
+
} catch {
|
|
29
|
+
process.stderr.write(
|
|
30
|
+
"Brand MCP runtime is missing. Reinstall the reviewed package.\n",
|
|
31
|
+
);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Resolve the reviewed bundle from this file, so users never type its path.
|
|
37
|
+
process.env.PROXY_AGENT_BUNDLE_DIR = bundleDir;
|
|
38
|
+
await import(pathToFileURL(runtimeEntry).href);
|
package/bundle/setup.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const bundleDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const brand = JSON.parse(await readFile(join(bundleDir, "brand.json"), "utf8"));
|
|
9
|
+
const launcher = join(bundleDir, "run-mcp.mjs");
|
|
10
|
+
const skill = join(bundleDir, "skill", brand.skillName);
|
|
11
|
+
const codexSkillTarget = join(homedir(), ".agents", "skills", brand.skillName);
|
|
12
|
+
const claudeSkillTarget = join(homedir(), ".claude", "skills", brand.skillName);
|
|
13
|
+
const quotedLauncher =
|
|
14
|
+
process.platform === "win32"
|
|
15
|
+
? `"${launcher}"`
|
|
16
|
+
: `'${launcher.replaceAll("'", "'\\''")}'`;
|
|
17
|
+
|
|
18
|
+
process.stdout.write(
|
|
19
|
+
`Brand package: ${brand.npmPackageName ?? brand.displayName}\n` +
|
|
20
|
+
`MCP launcher: ${launcher}\n` +
|
|
21
|
+
`Agent Skill folder: ${skill}\n` +
|
|
22
|
+
`Codex user Skill target: ${codexSkillTarget}\n` +
|
|
23
|
+
`Claude Code user Skill target: ${claudeSkillTarget}\n` +
|
|
24
|
+
`Copy the entire Skill folder to the target for the client you use; check before replacing an existing installation.\n` +
|
|
25
|
+
`\nCodex config.toml:\n` +
|
|
26
|
+
`[mcp_servers.${brand.serverName}]\n` +
|
|
27
|
+
`command = "node"\n` +
|
|
28
|
+
`args = [${JSON.stringify(launcher)}]\n` +
|
|
29
|
+
`env_vars = ["PROXY_AGENT_API_ORIGIN", "PROXY_AGENT_API_KEY"]\n` +
|
|
30
|
+
`\nClaude Code:\n` +
|
|
31
|
+
`claude mcp add --scope user --transport stdio ${brand.serverName} -- node ${quotedLauncher}\n` +
|
|
32
|
+
`The client process must inherit PROXY_AGENT_API_ORIGIN and PROXY_AGENT_API_KEY from a trusted local source.\n` +
|
|
33
|
+
`Ask a local Agent with machine access to read this output, install the Skill, register the MCP launcher, and verify the server name and tools. Never paste the API key into chat or command arguments. If the Agent lacks local access, follow these steps manually.\n`,
|
|
34
|
+
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: puraroute-proxy
|
|
3
|
+
description: Manage proxy resources with PuraRoute, assemble connection settings, troubleshoot failures, and use connected MCP tools for approved purchases or renewals.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PuraRoute
|
|
7
|
+
|
|
8
|
+
Use this Skill for proxy tasks with PuraRoute. Discover the connected tools; the bundled public contract declares the supported operations. Select the instance and resource before using account or connection data.
|
|
9
|
+
|
|
10
|
+
If the local MCP server is absent, explain concepts or format connection settings from details the user already has. Mark those details unverified. Live balance, stock, account, order, credential, purchase, and renewal work requires the connected MCP server. See [setup](references/setup.md). Never ask for an account API key or proxy password in chat just to diagnose a missing tool.
|
|
11
|
+
|
|
12
|
+
## Route the request
|
|
13
|
+
|
|
14
|
+
| Goal | First action | Guide |
|
|
15
|
+
| ------------------------ | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
16
|
+
| Use an existing proxy | Read resources and status for the selected instance. | [Workflows](references/workflows.md), [formats](references/connection-formats.md) |
|
|
17
|
+
| Select or purchase | Gather workload, region, protocol, duration, and traffic needs; read live offers. | [Products](references/products.md), [workflows](references/workflows.md) |
|
|
18
|
+
| Renew | Read target resource and current quote before a write. | [Workflows](references/workflows.md) |
|
|
19
|
+
| Fix a connection | Locate the failing stage and inspect current resource data. | [Troubleshooting](references/troubleshooting.md) |
|
|
20
|
+
| Configure an application | Confirm the user-selected application and supported format. | [Formats](references/connection-formats.md), [workflows](references/workflows.md) |
|
|
21
|
+
|
|
22
|
+
Use only named business tools in the [contract map](references/verified-contract.md). Do not construct arbitrary HTTP calls. Never pass a target host, origin, account API key, or arbitrary URL as a tool argument. The selected instance's live results determine prices, resources, stock, status, and connection credentials.
|
|
23
|
+
|
|
24
|
+
Before a purchase or renewal, show the instance, target, quantity or term, and live price; obtain authorization for that exact write. After an uncertain write, read the original order or resource state before considering any repeat. A returned connection string does not change the Agent host or operating system network exit. Configure only the user-selected application when local control and permission exist, then verify from that application. Otherwise provide a copyable configuration and say it has not been applied.
|
|
25
|
+
|
|
26
|
+
Treat proxy credentials as sensitive. Show a full password only for an explicit connection configuration request; never expose the account API key. For Chinese guidance, read the [Chinese guide](references/zh-CN.md). The [fixture cases](examples/connection-cases.md) use synthetic values.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Synthetic connection cases
|
|
2
|
+
|
|
3
|
+
These fictional cases use reserved `.example.test` hosts and sanitized fields. They illustrate the operations in this brand's public contract, not live PuraRoute resource values.
|
|
4
|
+
|
|
5
|
+
## Existing proxy
|
|
6
|
+
|
|
7
|
+
**Request:** “Use my existing static proxy.”
|
|
8
|
+
|
|
9
|
+
**Expected tool sequence:** Call `listStaticResources`, identify the selected item, and inspect its status, `host`, `port`, `protocol`, `account`, and `password`. If the selected item is absent or a required field is missing, report that the configuration is incomplete. Do not use the allocated `ip` as the dial host without evidence.
|
|
10
|
+
|
|
11
|
+
## HTTP and SOCKS5
|
|
12
|
+
|
|
13
|
+
**Fictional static result:** `protocol=HTTP`, `host=static.example.test`, `port=3128`, `account=case@user`, `password=p:a/ss`, `ip=192.0.2.10`.
|
|
14
|
+
|
|
15
|
+
**Expected HTTP configuration:** `http://case%40user:p%3Aa%2Fss@static.example.test:3128`.
|
|
16
|
+
|
|
17
|
+
**Fictional dynamic result:** `host=dynamic.example.test`, `socks5_port=1080`, `proxy_username=case-user`, `password=case-pass`.
|
|
18
|
+
|
|
19
|
+
**Expected SOCKS5 configuration:** `socks5://case-user:case-pass@dynamic.example.test:1080`, if the selected application accepts that scheme. Call `generateDynamicProxyCredentials` only when the user needs a usable dynamic connection. A generated credential does not itself apply the configuration.
|
|
20
|
+
|
|
21
|
+
## Expired resource
|
|
22
|
+
|
|
23
|
+
**Request:** “Why does my static proxy fail?”
|
|
24
|
+
|
|
25
|
+
**Expected tool sequence:** Use `listStaticResources` to confirm the resource status, then `quoteStaticRenewal` if renewal is available and the user wants a price. Report an observed expired status; never invent an offer or silently submit `createStaticRenewalOrder`.
|
|
26
|
+
|
|
27
|
+
## Uncertain purchase
|
|
28
|
+
|
|
29
|
+
**Request:** “My static purchase timed out.”
|
|
30
|
+
|
|
31
|
+
**Expected tool sequence:** Preserve the original `request_no`, use `listStaticPurchaseOrders` or `getStaticPurchaseOrder` to reconcile the order, then inspect resources if needed. Do not submit `createStaticPurchaseOrder` again with a new request number. If the lookup cannot settle the state, report uncertainty.
|
|
32
|
+
|
|
33
|
+
The configuration has not been applied unless the selected application was actually changed and its exit checked.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Connection formats
|
|
2
|
+
|
|
3
|
+
Use connection details returned for the selected PuraRoute resource and instance. The bundled public contract identifies the field paths below. The example values are synthetic; they are not live endpoints or credentials.
|
|
4
|
+
|
|
5
|
+
For `generateDynamicProxyCredentials`, read `data.host`, `data.proxy_username`, `data.password`, and the port for the chosen protocol: `data.http_port` or `data.socks5_port`. For `listStaticResources`, inspect an item in `data.list` and read `host`, `port`, `account`, `password`, and `protocol`. Keep `ip` separate from the dial host. If the selected item or required field is absent, report an incomplete configuration.
|
|
6
|
+
|
|
7
|
+
## Field check before formatting
|
|
8
|
+
|
|
9
|
+
| Field | Use | Check |
|
|
10
|
+
| --------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| Protocol | Select the application's HTTP or SOCKS5 mode. | The resource and application both support it. Do not infer it from a port. |
|
|
12
|
+
| Connection host | Hostname or address the client dials. | Prefer the field documented as the proxy entry point. An allocated IP may describe the exit instead. |
|
|
13
|
+
| Port | Port paired with that host and protocol. | Use the current value from the selected resource; do not reuse another product's port. |
|
|
14
|
+
| Username/password | Proxy authentication, when required. | Use only returned proxy credentials. The account API key is unrelated. |
|
|
15
|
+
| Allowlisted source IP | An alternative or additional access condition only if documented. | Confirm the product's rule and the user's actual source IP before describing it as active. |
|
|
16
|
+
|
|
17
|
+
If any required field or the authentication mode is unknown, label the configuration incomplete. Do not guess a username, password, default port, domain, or browser import format.
|
|
18
|
+
|
|
19
|
+
## URL-style configuration
|
|
20
|
+
|
|
21
|
+
When the target application accepts a proxy URL, percent-encode **each credential component** separately. Keep the host unescaped as a hostname; bracket a literal IPv6 address. The general shape is:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
http://ENCODED_USER:ENCODED_PASSWORD@CONNECTION_HOST:PORT
|
|
25
|
+
socks5://ENCODED_USER:ENCODED_PASSWORD@CONNECTION_HOST:PORT
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For a fictional HTTP result with host `static.example.test`, port `3128`, username `case@user`, and password `p:a/ss`, the URL is:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
http://case%40user:p%3Aa%2Fss@static.example.test:3128
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Enter unescaped credentials when the application offers separate username and password fields. Do not append region or session tokens to the username unless the reviewed product instructions explicitly define that syntax. Some clients distinguish SOCKS5 DNS modes and accept different URL schemes; check the chosen client's documentation instead of assuming `socks5://` works everywhere.
|
|
35
|
+
|
|
36
|
+
## Delivery and verification
|
|
37
|
+
|
|
38
|
+
For an explicit setup request, provide the complete, copyable format that the chosen application accepts. In broader reports, mask the proxy password. Never put the account API key into a proxy URL or echo it back. Avoid saving a full credential URL in a shared file, log, screenshot, or shell command history.
|
|
39
|
+
|
|
40
|
+
Once applied, run a read-only IP check **from the selected application** and compare the observed exit with the resource's expected region or IP when such an expectation is documented. A retrieved connection string alone proves neither application configuration nor a changed network exit. See [workflows](workflows.md) for the setup sequence.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Product selection
|
|
2
|
+
|
|
3
|
+
Use PuraRoute MCP results for current products, selectable regions, inventory, prices, limits, wallet balance, and account resources. The bundled contract defines fields; it does not promise a fixed gateway, port, rotation interval, or price. If a required read tool is missing, state the gap.
|
|
4
|
+
|
|
5
|
+
Ask first whether the user has a resource. For a new choice, establish the target application, exit region, HTTP or SOCKS5 requirement, expected traffic and concurrency, duration, and whether a stable exit identity is needed. Compare only offers supported by live results from the selected instance.
|
|
6
|
+
|
|
7
|
+
HTTP and SOCKS5 are different client protocols. Rotation and sticky sessions describe how exit identity may change; confirm actual behavior from the selected product. An allocated IP may describe the exit and may differ from the connection host. Verify IP allowlisting rules before treating them as active authentication.
|
|
8
|
+
|
|
9
|
+
Show billing unit, quantity, total, stock, and wallet balance before a purchase. Preserve decimal money and traffic values as returned strings. Never invent an unavailable price or region.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Local MCP setup for PuraRoute
|
|
2
|
+
|
|
3
|
+
Install the reviewed PuraRoute npm package from its approved publisher. It includes this brand's contract, MCP launcher, and Agent Skill and the shared neutral runtime code.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
npm install --global @agent-proxy-kit/puraroute-mcp-server
|
|
7
|
+
puraroute-proxy-mcp --setup
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The setup output prints the absolute MCP launcher and Skill source folder. npm installs both files, but does not register the MCP server or activate the Skill in an Agent client. With the user's permission, an Agent that can operate this machine can complete the following steps. Without local access, provide the exact steps for the user to run and say what remains unconfigured.
|
|
11
|
+
|
|
12
|
+
1. Run `puraroute-proxy-mcp --setup` and use its actual installed paths. Copy the entire Skill folder, including `SKILL.md` and `references/`, to the chosen client's user Skill directory: `~/.agents/skills/puraroute-proxy` for Codex or `~/.claude/skills/puraroute-proxy` for Claude Code. On Windows, use the corresponding directories under the user's home folder. Inspect an existing target before replacing it.
|
|
13
|
+
2. Register the printed launcher as a local stdio MCP server. For Codex, use the printed `config.toml` block. For Claude Code, use the printed `claude mcp add --scope user` command. The launcher finds its bundled contract automatically; users do not set a bundle path.
|
|
14
|
+
3. Have the user arrange for `PROXY_AGENT_API_ORIGIN` and `PROXY_AGENT_API_KEY` to be inherited by the Agent client from a trusted local source. The origin is an approved HTTPS **origin without a path**, query, or fragment; the origin and key must belong to the same instance. Do not ask the user to paste the API key into chat, put it in command arguments or Skill text, or log its value.
|
|
15
|
+
4. Restart or refresh the client. Check that it loads the `puraroute-proxy` Skill and connects to MCP service `puraroute-proxy-mcp`; compare `tools/list` with the bundled `bundle-manifest.json`. Do not call purchase, renewal, or account-write tools merely to verify setup.
|
|
16
|
+
|
|
17
|
+
The optional timeout is `PROXY_AGENT_REQUEST_TIMEOUT_MS`. Keep the key out of shell history and project files.
|
|
18
|
+
|
|
19
|
+
For installation help, use https://puraroute.com. Do not substitute another brand's API origin or bundle.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Start with the selected instance, resource ID, live status, and the exact failing stage: MCP startup, customer API call, proxy connection, or application exit test. A failure at one stage does not prove a failure at another. Do not include an account API key, proxy password, request body, or full API URL in diagnostic summaries.
|
|
4
|
+
|
|
5
|
+
| Observation | Check | Safe next action |
|
|
6
|
+
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| MCP tools are absent or startup fails | Reviewed contract path, adapter policy, HTTPS instance origin, and whether the client process received its API key. | Report the missing configuration or contract; do not invent operation names. |
|
|
8
|
+
| `business_error` under HTTP 200 | Business code, safe message, trace ID, selected instance, and resource status. | Use the documented meaning of that code; if unknown, report the code without guessing. |
|
|
9
|
+
| `http_error` 401/403 | Instance/key pairing and actual business permission, without exposing the key. | Ask the operator to check the secure local configuration and account authorization. |
|
|
10
|
+
| `http_error` 5xx, `malformed_json`, or `network_error` | Service health, trace ID, and whether the operation was a read or write. | Preserve diagnostics; do not convert an unknown write result into a new purchase. |
|
|
11
|
+
| `timeout` on a read | Whether the read can be safely repeated and whether the instance is reachable. | Retry only a suitable read if useful; keep the same instance. |
|
|
12
|
+
| `uncertain_write_result` | Original operation and request number. | Query order/resource state with an approved read tool; stop if no such tool exists. Do not repeat the write automatically. |
|
|
13
|
+
| Proxy authentication or connection failure | Current resource status, verified protocol/host/port, proxy credentials, and any documented allowlist condition. | Correct only the field supported by evidence. Never substitute the account API key as a proxy password. |
|
|
14
|
+
| Resource is expired or depleted | Live status, expiry or quota, and a verified renewal offer. | Show price and target before an authorized renewal. |
|
|
15
|
+
| Application still shows its original exit IP | Whether that specific application accepted and applied the proxy settings; whether the test ran inside it. | Recheck its proxy mode and perform another read-only exit test. Do not claim the whole OS changed. |
|
|
16
|
+
|
|
17
|
+
When contacting support, include operation ID, safe business code or HTTP status, trace ID if present, and the approximate time. Keep proxy credentials and account keys out of the report. For a write timeout, preserve the original request number for reconciliation.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Bundled business tools
|
|
2
|
+
|
|
3
|
+
The PuraRoute public bundle declares 26 operations under `/gin/open-api/v1/`. Discover the tools available in the connected MCP session and use their advertised input schemas. The local runtime sends requests only to the configured HTTPS instance origin.
|
|
4
|
+
|
|
5
|
+
- `createDynamicAccount`: Create a dynamic proxy account
|
|
6
|
+
- `createStaticPurchaseOrder`: Place a static proxy order
|
|
7
|
+
- `createStaticRenewalOrder`: Renew a static proxy resource
|
|
8
|
+
- `generateDynamicProxyCredentials`: Generate dynamic proxy credentials
|
|
9
|
+
- `getDynamicAccount`: Get dynamic account details
|
|
10
|
+
- `getDynamicTrafficBalance`: Get dynamic traffic balance
|
|
11
|
+
- `getDynamicTrafficOrder`: Get dynamic traffic order
|
|
12
|
+
- `getStaticInventory`: Check static proxy inventory
|
|
13
|
+
- `getStaticPurchaseOrder`: Get static purchase order
|
|
14
|
+
- `getStaticRenewalOrder`: Get static renewal order
|
|
15
|
+
- `getWalletBalance`: Get wallet balance
|
|
16
|
+
- `listDynamicAccounts`: List dynamic proxy accounts
|
|
17
|
+
- `listDynamicDailyUsage`: List daily dynamic traffic usage
|
|
18
|
+
- `listDynamicRegions`: List dynamic proxy regions
|
|
19
|
+
- `listDynamicTrafficOrders`: List dynamic traffic orders
|
|
20
|
+
- `listDynamicTrafficProducts`: List dynamic traffic products
|
|
21
|
+
- `listEffectiveDynamicTrafficLots`: List active traffic lots
|
|
22
|
+
- `listStaticPurchaseOrders`: List static purchase orders
|
|
23
|
+
- `listStaticRegions`: List static proxy regions
|
|
24
|
+
- `listStaticRenewalOrders`: List static renewal orders
|
|
25
|
+
- `listStaticResources`: List static proxy resources
|
|
26
|
+
- `purchaseDynamicTraffic`: Purchase dynamic traffic
|
|
27
|
+
- `quoteStaticPurchase`: Quote static proxy purchase
|
|
28
|
+
- `quoteStaticRenewal`: Quote static proxy renewal
|
|
29
|
+
- `setDynamicAccountStatus`: Set dynamic account status
|
|
30
|
+
- `updateDynamicAccountLimit`: Update dynamic account limit
|
|
31
|
+
|
|
32
|
+
The business adapter treats HTTP 200 with `code=0` as success for this sample contract; `msg`, `data`, and `next` are response envelope fields. Do not treat transport success as a completed purchase. The bundle's adapter policy is the authority for success semantics.
|
|
33
|
+
|
|
34
|
+
For `listDynamicRegions`, use `query.level=COUNTRY` without `parent_code`; for `PROVINCE` or `CITY`, provide the parent region's `region_code` as `query.parent_code`. The bundled tool constraints enforce this rule.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Operational workflows
|
|
2
|
+
|
|
3
|
+
The MCP server exposes the operations in the PuraRoute public contract. Discover their names and input schemas at runtime. Keep the instance and resource identifiers from the same API origin throughout a workflow.
|
|
4
|
+
|
|
5
|
+
## Use an existing resource
|
|
6
|
+
|
|
7
|
+
1. Identify the requested instance and resource. If the user has no resource ID, use an approved list/search operation if the connected contract provides one.
|
|
8
|
+
2. Read the resource's current status and connection details. A stale result or a different instance's data cannot establish usability.
|
|
9
|
+
3. Check protocol, connection host, port, authentication mode, and required credentials. Distinguish a dial host from an allocated/exit IP. Read [connection formats](connection-formats.md) before constructing a URL.
|
|
10
|
+
4. Give the requested configuration or a precise list of missing fields. Do not purchase a replacement merely because the first lookup failed.
|
|
11
|
+
|
|
12
|
+
## Choose and purchase
|
|
13
|
+
|
|
14
|
+
1. Gather workload and location requirements using [product selection](products.md), then read live product/price/stock and wallet results from the selected instance.
|
|
15
|
+
2. Present a purchase summary: instance, exact product or SKU, region or mode if applicable, quantity, billing unit, current total, and any target resource. Mark values that the API does not provide as unknown.
|
|
16
|
+
3. After the user authorizes that concrete purchase, submit the specific approved write operation once with its original request number. Do not create a generic HTTP call.
|
|
17
|
+
4. On a confirmed success, use approved read tools to confirm the order/resource and then read its connection details. A 2xx HTTP response alone is not business success; inspect the business code.
|
|
18
|
+
5. On `uncertain_write_result`, keep the original request number and query order/resource state with a read-only tool. If the contract lacks a lookup, report uncertainty and stop. Never submit again with a fresh request number just to resolve a timeout.
|
|
19
|
+
|
|
20
|
+
## Renew
|
|
21
|
+
|
|
22
|
+
Read the target resource and its current expiry/status, then read the available renewal offer and price. Show the instance, resource, extension term or quantity, and total before the write. Follow the same single-write and uncertain-result recovery rules as purchase. Do not assume that an expired resource can be renewed until the live API confirms the action.
|
|
23
|
+
|
|
24
|
+
## Configure a selected application
|
|
25
|
+
|
|
26
|
+
1. Confirm the user-selected application and its accepted proxy protocol and field format. Assemble the exact verified connection data.
|
|
27
|
+
2. If the Agent has local control and the user asked it to configure that application, change only that application's proxy settings. Avoid changing system-wide or other applications' settings as a side effect.
|
|
28
|
+
3. Make a read-only IP check from within that application. Report the observed exit IP and whether it matches a verified expectation. If the application cannot perform such a check, say verification is pending.
|
|
29
|
+
4. Without local control, provide a copyable configuration and state plainly that it has not been applied.
|
|
30
|
+
|
|
31
|
+
## Answer state explicitly
|
|
32
|
+
|
|
33
|
+
A useful answer separates **resource found**, **connection details complete**, **configuration applied**, and **exit verified**. These are different outcomes. Give the selected instance/resource and status, any remaining missing field or read operation, and the next safe action. Include full proxy credentials only when the user asked for a complete configuration.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# PuraRoute 代理操作指南
|
|
2
|
+
|
|
3
|
+
先确认用户要使用现有代理、选购或续费、排查连接,还是配置指定应用。当前品牌的本地 MCP 工具来自随包公开契约;实时产品、库存、余额、资源状态和凭据都要查询选中的实例。缺少 MCP 时,可解释概念或整理用户已提供的连接字段,但不能声称账户和资源已核实;安装方式见[本地配置](setup.md)。不要要求用户在聊天中粘贴账户 API Key 或代理密码。
|
|
4
|
+
|
|
5
|
+
npm 包已经包含 Skill 文件,但不会自动把它加载到 Agent 客户端。可让有本机操作权限的 Agent 运行 `puraroute-proxy-mcp --setup`,按输出将整个 Skill 文件夹复制到 Codex 用户目录 `~/.agents/skills/puraroute-proxy` 或 Claude Code 用户目录 `~/.claude/skills/puraroute-proxy`,再注册 MCP、从本机安全来源配置实例地址与 API Key,重启并验证工具列表。没有本机权限时,只提供具体手动步骤,不声称已完成配置。
|
|
6
|
+
|
|
7
|
+
选型前确认目标应用、地区、HTTP 或 SOCKS5 协议、用量、并发、使用时长及是否需要稳定出口。轮换、粘性会话和 IP 白名单的具体行为必须以该实例的已审核资料或实时结果为准。`listDynamicRegions` 查询国家时不传 `parent_code`;查询省州或城市时须传上级 `region_code`。
|
|
8
|
+
|
|
9
|
+
完整连接配置需要协议、连接入口主机、端口和适用的认证字段。动态代理凭据可查看 `generateDynamicProxyCredentials` 的 `data.host`、`data.proxy_username`、`data.password`、`data.http_port` 或 `data.socks5_port`;静态资源可查看 `listStaticResources` 的 `data.list` 条目。分配 IP 可能是出口 IP,不能自动当成连接主机。URL 格式要分别编码用户名和密码;没有字段就说明配置不完整,不猜网关或端口。
|
|
10
|
+
|
|
11
|
+
购买或续费前展示实例、目标资源或产品、数量或时长、实时总价,取得对该写操作的授权。若写操作超时,保留原请求号,先查订单或资源;不要盲目重试。HTTP 成功不等于业务成功,应核对业务码。
|
|
12
|
+
|
|
13
|
+
只有在用户指定应用、Agent 有本机控制权且获得相应授权时,才修改该应用的代理设置,并从应用内部检查出口 IP。否则给出可复制的配置并明确说“尚未应用”。读取连接信息本身不会改变操作系统出口。概览中隐藏代理密码,绝不展示账户 API Key。
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"listDynamicRegions": {
|
|
3
|
+
"oneOf": [
|
|
4
|
+
{
|
|
5
|
+
"description": "COUNTRY lookup must not include parent_code.",
|
|
6
|
+
"properties": {
|
|
7
|
+
"query": {
|
|
8
|
+
"properties": {
|
|
9
|
+
"level": { "const": "COUNTRY" }
|
|
10
|
+
},
|
|
11
|
+
"not": { "required": ["parent_code"] }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"description": "PROVINCE and CITY lookups require parent_code.",
|
|
17
|
+
"properties": {
|
|
18
|
+
"query": {
|
|
19
|
+
"properties": {
|
|
20
|
+
"level": { "enum": ["PROVINCE", "CITY"] }
|
|
21
|
+
},
|
|
22
|
+
"required": ["parent_code"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/brand.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function validateBrandProfile(value) {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5
|
+
throw new Error("Invalid brand.json");
|
|
6
|
+
const profile = value;
|
|
7
|
+
if (typeof profile.id !== "string" ||
|
|
8
|
+
!/^[a-z][a-z0-9-]{1,62}$/.test(profile.id) ||
|
|
9
|
+
typeof profile.displayName !== "string" ||
|
|
10
|
+
!/^[\p{L}\p{N}][\p{L}\p{N} .&'-]{1,79}$/u.test(profile.displayName) ||
|
|
11
|
+
typeof profile.serverName !== "string" ||
|
|
12
|
+
!/^[a-z][a-z0-9-]{1,62}$/.test(profile.serverName) ||
|
|
13
|
+
typeof profile.skillName !== "string" ||
|
|
14
|
+
!/^[a-z][a-z0-9-]{1,62}$/.test(profile.skillName) ||
|
|
15
|
+
typeof profile.version !== "string" ||
|
|
16
|
+
!/^\d+\.\d+\.\d+$/.test(profile.version) ||
|
|
17
|
+
typeof profile.supportUrl !== "string")
|
|
18
|
+
throw new Error("Invalid brand.json");
|
|
19
|
+
if (profile.npmPackageName !== undefined &&
|
|
20
|
+
(typeof profile.npmPackageName !== "string" ||
|
|
21
|
+
!/^@[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/.test(profile.npmPackageName)))
|
|
22
|
+
throw new Error("Invalid brand.json npm package name");
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(profile.supportUrl);
|
|
25
|
+
if (url.protocol !== "https:" || url.username || url.password)
|
|
26
|
+
throw new Error("Invalid brand.json");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error("Invalid brand.json");
|
|
30
|
+
}
|
|
31
|
+
return profile;
|
|
32
|
+
}
|
|
33
|
+
export async function readBrandProfile(bundleDir) {
|
|
34
|
+
try {
|
|
35
|
+
return validateBrandProfile(JSON.parse(await readFile(join(bundleDir, "brand.json"), "utf8")));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error("Missing or invalid brand.json in bundle");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Ajv } from "@modelcontextprotocol/server/validators/ajv";
|
|
2
|
+
// OpenAPI formats used by approved inputs. The numeric formats must be checked
|
|
3
|
+
// explicitly because unknown Ajv formats otherwise become warnings and no-op rules.
|
|
4
|
+
export function newInputValidator() {
|
|
5
|
+
const validator = new Ajv({ strict: false, allErrors: true });
|
|
6
|
+
validator.addFormat("int32", {
|
|
7
|
+
type: "number",
|
|
8
|
+
validate: (value) => Number.isInteger(value) && value >= -2147483648 && value <= 2147483647,
|
|
9
|
+
});
|
|
10
|
+
validator.addFormat("int64", {
|
|
11
|
+
type: "number",
|
|
12
|
+
validate: (value) => Number.isSafeInteger(value),
|
|
13
|
+
});
|
|
14
|
+
validator.addFormat("date", {
|
|
15
|
+
type: "string",
|
|
16
|
+
validate: (value) => {
|
|
17
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
18
|
+
return false;
|
|
19
|
+
const date = new Date(`${value}T00:00:00.000Z`);
|
|
20
|
+
return (!Number.isNaN(date.valueOf()) && date.toISOString().startsWith(value));
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
validator.addFormat("date-time", {
|
|
24
|
+
type: "string",
|
|
25
|
+
validate: (value) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && !Number.isNaN(Date.parse(value)),
|
|
26
|
+
});
|
|
27
|
+
validator.addFormat("uri", {
|
|
28
|
+
type: "string",
|
|
29
|
+
validate: (value) => {
|
|
30
|
+
try {
|
|
31
|
+
return Boolean(new URL(value).protocol);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
validator.addFormat("password", { type: "string", validate: () => true });
|
|
39
|
+
return validator;
|
|
40
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import { dereference, validate } from "@scalar/openapi-parser";
|
|
6
|
+
import { collectOperations, } from "./operations.js";
|
|
7
|
+
const obj = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
8
|
+
? value
|
|
9
|
+
: {};
|
|
10
|
+
function rejectExternalReferences(value, location = "contract") {
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
value.forEach((item, index) => rejectExternalReferences(item, `${location}[${index}]`));
|
|
13
|
+
}
|
|
14
|
+
else if (value !== null && typeof value === "object") {
|
|
15
|
+
const record = value;
|
|
16
|
+
if (typeof record.$ref === "string" && !record.$ref.startsWith("#/")) {
|
|
17
|
+
throw new Error(`${location}: external $ref is unsupported`);
|
|
18
|
+
}
|
|
19
|
+
for (const [key, item] of Object.entries(record))
|
|
20
|
+
rejectExternalReferences(item, `${location}.${key}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function checkSecurity(document) {
|
|
24
|
+
const schemes = obj(obj(document.components).securitySchemes);
|
|
25
|
+
const goodSchemes = Object.entries(schemes)
|
|
26
|
+
.filter(([, value]) => {
|
|
27
|
+
const scheme = obj(value);
|
|
28
|
+
return (scheme.type === "apiKey" &&
|
|
29
|
+
scheme.in === "header" &&
|
|
30
|
+
scheme.name === "X-API-Key");
|
|
31
|
+
})
|
|
32
|
+
.map(([name]) => name);
|
|
33
|
+
if (goodSchemes.length !== 1 || Object.keys(schemes).length !== 1) {
|
|
34
|
+
throw new Error("Contract must declare only the X-API-Key header security scheme");
|
|
35
|
+
}
|
|
36
|
+
const required = JSON.stringify([{ [goodSchemes[0]]: [] }]);
|
|
37
|
+
if (JSON.stringify(document.security) !== required)
|
|
38
|
+
throw new Error("Contract must require X-API-Key globally");
|
|
39
|
+
for (const [path, itemValue] of Object.entries(obj(document.paths))) {
|
|
40
|
+
for (const [method, value] of Object.entries(obj(itemValue))) {
|
|
41
|
+
if (["get", "post", "put", "patch", "delete"].includes(method) &&
|
|
42
|
+
obj(value).security !== undefined) {
|
|
43
|
+
if (JSON.stringify(obj(value).security) !== required)
|
|
44
|
+
throw new Error(`${method.toUpperCase()} ${path}: unsupported security override`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function applyToolConstraints(contractPath, operations) {
|
|
50
|
+
let source;
|
|
51
|
+
try {
|
|
52
|
+
source = await readFile(join(dirname(contractPath), "tool-constraints.json"), "utf8");
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === "ENOENT")
|
|
56
|
+
return;
|
|
57
|
+
throw new Error("Cannot read tool-constraints.json");
|
|
58
|
+
}
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(source);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error("Invalid tool-constraints.json");
|
|
65
|
+
}
|
|
66
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
67
|
+
throw new Error("Invalid tool-constraints.json");
|
|
68
|
+
const byName = new Map(operations.map((operation) => [operation.operationId, operation]));
|
|
69
|
+
for (const [name, constraint] of Object.entries(parsed)) {
|
|
70
|
+
const operation = byName.get(name);
|
|
71
|
+
if (!operation)
|
|
72
|
+
throw new Error(`Unknown tool constraint: ${name}`);
|
|
73
|
+
if (!constraint ||
|
|
74
|
+
typeof constraint !== "object" ||
|
|
75
|
+
Array.isArray(constraint))
|
|
76
|
+
throw new Error(`Invalid tool constraint: ${name}`);
|
|
77
|
+
operation.inputConstraints = constraint;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function loadCatalog(contractPath) {
|
|
81
|
+
const bytes = await readFile(contractPath);
|
|
82
|
+
const source = bytes.toString("utf8");
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = obj(YAML.parse(source));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error("Invalid OpenAPI JSON/YAML document");
|
|
89
|
+
}
|
|
90
|
+
const version = parsed.openapi;
|
|
91
|
+
if (typeof version !== "string" || !/^3\.(0|1)\.\d+$/.test(version)) {
|
|
92
|
+
throw new Error("Unsupported OpenAPI version; expected 3.0.x or 3.1.x");
|
|
93
|
+
}
|
|
94
|
+
rejectExternalReferences(parsed);
|
|
95
|
+
checkSecurity(parsed);
|
|
96
|
+
const servers = parsed.servers;
|
|
97
|
+
if (!Array.isArray(servers) || servers.length !== 1)
|
|
98
|
+
throw new Error("Contract must declare exactly one server URL");
|
|
99
|
+
const serverUrl = obj(servers[0]).url;
|
|
100
|
+
if (typeof serverUrl !== "string")
|
|
101
|
+
throw new Error("Unsupported server URL");
|
|
102
|
+
let serverPath = serverUrl;
|
|
103
|
+
if (/^https?:\/\//i.test(serverUrl)) {
|
|
104
|
+
let parsedUrl;
|
|
105
|
+
try {
|
|
106
|
+
parsedUrl = new URL(serverUrl);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new Error("Unsupported server URL");
|
|
110
|
+
}
|
|
111
|
+
if (parsedUrl.username ||
|
|
112
|
+
parsedUrl.password ||
|
|
113
|
+
parsedUrl.search ||
|
|
114
|
+
parsedUrl.hash)
|
|
115
|
+
throw new Error("Unsupported server URL");
|
|
116
|
+
serverPath = parsedUrl.pathname;
|
|
117
|
+
}
|
|
118
|
+
if (!/^\/[A-Za-z0-9/_-]*$/.test(serverPath) || serverPath.includes(".."))
|
|
119
|
+
throw new Error("Unsupported server URL; expected a safe base path");
|
|
120
|
+
// The contract authority is metadata only. Runtime requests use the configured HTTPS origin.
|
|
121
|
+
const basePath = serverPath.replace(/\/$/, "") || "";
|
|
122
|
+
const validation = await validate(source);
|
|
123
|
+
if (!validation.valid)
|
|
124
|
+
throw new Error(`OpenAPI validation failed: ${validation.errors.map(String).join("; ")}`);
|
|
125
|
+
const resolved = await dereference(source);
|
|
126
|
+
if (resolved.errors?.length)
|
|
127
|
+
throw new Error(`OpenAPI dereference failed: ${resolved.errors.map(String).join("; ")}`);
|
|
128
|
+
const document = obj(resolved.schema);
|
|
129
|
+
const operations = collectOperations(document, basePath, version);
|
|
130
|
+
await applyToolConstraints(contractPath, operations);
|
|
131
|
+
return {
|
|
132
|
+
contractSha256: createHash("sha256").update(bytes).digest("hex"),
|
|
133
|
+
serverBasePath: basePath,
|
|
134
|
+
operations,
|
|
135
|
+
openapiVersion: version,
|
|
136
|
+
};
|
|
137
|
+
}
|