@quidli/connect-mcp 0.2.0 → 0.4.0
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/README.md +44 -23
- package/dist/anonymous-quota.d.ts +28 -0
- package/dist/anonymous-quota.js +78 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +4 -1
- package/dist/config.d.ts +14 -3
- package/dist/config.js +31 -6
- package/dist/deeplink.d.ts +1 -1
- package/dist/deeplink.js +5 -1
- package/dist/hosted-gate.d.ts +21 -0
- package/dist/hosted-gate.js +21 -0
- package/dist/http-auth.d.ts +7 -4
- package/dist/http-auth.js +5 -1
- package/dist/index-http.js +0 -0
- package/dist/index.js +0 -0
- package/dist/jsonrpc.d.ts +2 -0
- package/dist/jsonrpc.js +14 -0
- package/dist/metadata.js +1 -1
- package/dist/mount.d.ts +3 -0
- package/dist/mount.js +17 -8
- package/dist/schemas.d.ts +18 -0
- package/dist/schemas.js +6 -0
- package/dist/tools/register.d.ts +1 -1
- package/dist/tools/register.js +20 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,38 +8,44 @@ Use [Quidli Connect](https://connect.quid.li) from Cursor, Claude Desktop, or an
|
|
|
8
8
|
| | **Hosted** (`mcp.connect.quid.li`) | **Local** (`npx @quidli/connect-mcp`) |
|
|
9
9
|
| --------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
10
10
|
| **Setup** | Add a URL in MCP settings | Run a local process via `npx` |
|
|
11
|
-
| **Auth** |
|
|
12
|
-
| **Pros** | No install; always on Quidli infrastructure
|
|
13
|
-
| **Cons** |
|
|
11
|
+
| **Auth** | Optional API key. Without a key: lookup/scores/agent/price under a shared anonymous quota | API key, x402 pay-per-call, or none for lookup/scores/agent |
|
|
12
|
+
| **Pros** | No install; try without a key; always on Quidli infrastructure | Wallet private key never leaves your machine; pay per call with USDC instead of an API key |
|
|
13
|
+
| **Cons** | Anonymous traffic shares a global rate limit; no pay-per-call billing | Requires Node.js 20+; `npx` may download the package on first run |
|
|
14
14
|
| **Limitations** | Cannot use x402 / wallet auth — do **not** send a private key to the hosted endpoint | x402 mode needs USDC on Base (mainnet `8453`); `connect_drop` requires an API key |
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
Get a Connect API key at [connect.quid.li](https://connect.quid.li) → **Enable API access
|
|
17
|
+
Get a Connect API key at [connect.quid.li](https://connect.quid.li) → **Enable API access** for higher limits, `connect_me`, and Smart Send.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
## Hosted (recommended)
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Zero local setup. Lookup, scores, agent, and price work without an API key (shared anonymous quota). Add a key for higher limits, your profile (`connect_me`), and Smart Send.
|
|
24
24
|
|
|
25
25
|
### Cursor
|
|
26
26
|
|
|
27
27
|
**If you already have other MCP servers:** skip the deeplink and [add manually](#cursor-manual) — the one-click install can overwrite your entire `mcp.json` in some Cursor versions.
|
|
28
28
|
|
|
29
|
-
[Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=quidli-connect&config=
|
|
29
|
+
[Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=quidli-connect&config=eyJ1cmwiOiJodHRwczovL21jcC5jb25uZWN0LnF1aWQubGkifQ==)
|
|
30
30
|
|
|
31
31
|
Or paste into **Cursor Settings → MCP → Add new MCP server**:
|
|
32
32
|
|
|
33
33
|
```
|
|
34
|
-
cursor://anysphere.cursor-deeplink/mcp/install?name=quidli-connect&config=
|
|
34
|
+
cursor://anysphere.cursor-deeplink/mcp/install?name=quidli-connect&config=eyJ1cmwiOiJodHRwczovL21jcC5jb25uZWN0LnF1aWQubGkifQ==
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Then open **Cursor Settings → MCP → quidli-connect** and set `headers.x-api-key` to your Connect API key.
|
|
38
|
-
|
|
39
37
|
#### Add manually {#cursor-manual}
|
|
40
38
|
|
|
41
39
|
Merge this into `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project) under `mcpServers` — do not replace your existing entries:
|
|
42
40
|
|
|
41
|
+
```json
|
|
42
|
+
"quidli-connect": {
|
|
43
|
+
"url": "https://mcp.connect.quid.li"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Optional — higher limits and Smart Send:
|
|
48
|
+
|
|
43
49
|
```json
|
|
44
50
|
"quidli-connect": {
|
|
45
51
|
"url": "https://mcp.connect.quid.li",
|
|
@@ -52,7 +58,7 @@ Merge this into `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project) un
|
|
|
52
58
|
### Claude Desktop
|
|
53
59
|
|
|
54
60
|
1. Open **Claude → Settings → Connectors** (or **Settings → Developer → Edit Config** on older versions).
|
|
55
|
-
2. Add a custom MCP connector with URL `https://mcp.connect.quid.li
|
|
61
|
+
2. Add a custom MCP connector with URL `https://mcp.connect.quid.li`. Optionally set header `x-api-key: <your-connect-api-key>`.
|
|
56
62
|
|
|
57
63
|
Or merge this into your config file and restart Claude:
|
|
58
64
|
|
|
@@ -63,10 +69,7 @@ Or merge this into your config file and restart Claude:
|
|
|
63
69
|
{
|
|
64
70
|
"mcpServers": {
|
|
65
71
|
"quidli-connect": {
|
|
66
|
-
"url": "https://mcp.connect.quid.li"
|
|
67
|
-
"headers": {
|
|
68
|
-
"x-api-key": "<your-connect-api-key>"
|
|
69
|
-
}
|
|
72
|
+
"url": "https://mcp.connect.quid.li"
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
}
|
|
@@ -83,13 +86,8 @@ If your Claude version does not support remote `url` connectors, use the local b
|
|
|
83
86
|
"-y",
|
|
84
87
|
"mcp-remote@latest",
|
|
85
88
|
"--http",
|
|
86
|
-
"https://mcp.connect.quid.li"
|
|
87
|
-
|
|
88
|
-
"x-api-key:${CONNECT_API_KEY}"
|
|
89
|
-
],
|
|
90
|
-
"env": {
|
|
91
|
-
"CONNECT_API_KEY": "<your-connect-api-key>"
|
|
92
|
-
}
|
|
89
|
+
"https://mcp.connect.quid.li"
|
|
90
|
+
]
|
|
93
91
|
}
|
|
94
92
|
}
|
|
95
93
|
}
|
|
@@ -97,7 +95,7 @@ If your Claude version does not support remote `url` connectors, use the local b
|
|
|
97
95
|
|
|
98
96
|
Requires **Node.js 20+** for the bridge. Restart Claude Desktop after saving. You should see a hammer icon in the chat input when tools are available.
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
Without a key, `initialize` / `tools/list` always succeed. Anonymous `tools/call` share a global quota (HTTP **429** when exceeded — get a key for higher limits). Placeholder keys still return **401**.
|
|
101
99
|
|
|
102
100
|
---
|
|
103
101
|
|
|
@@ -152,6 +150,26 @@ No API key. Authenticated calls pay automatically when the API returns HTTP 402.
|
|
|
152
150
|
|
|
153
151
|
Use `84532` for Base Sepolia when pointing at a staging API.
|
|
154
152
|
|
|
153
|
+
#### Option C — no credentials (lookup / scores / agent)
|
|
154
|
+
|
|
155
|
+
The MCP process starts without `CONNECT_API_KEY` or `EVM_PRIVATE_KEY`. `connect_lookup`, `connect_scores_*`, `connect_agent_prompt`, and `connect_get_price` call the API without auth. That succeeds when x402 is disabled (price `0`, typical for local). On production with x402 enabled, those tools return HTTP 402 until you set a key or wallet.
|
|
156
|
+
|
|
157
|
+
`connect_me`, `connect_drop`, and `connect_drop_balance` still require `CONNECT_API_KEY`.
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"mcpServers": {
|
|
162
|
+
"quidli-connect": {
|
|
163
|
+
"command": "npx",
|
|
164
|
+
"args": ["-y", "@quidli/connect-mcp"],
|
|
165
|
+
"env": {
|
|
166
|
+
"CONNECT_API_BASE_URL": "http://127.0.0.1:3011"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
155
173
|
### Claude Desktop
|
|
156
174
|
|
|
157
175
|
1. Open **Claude → Settings → Developer → Edit Config** (enable Developer mode first if you do not see it).
|
|
@@ -199,7 +217,7 @@ Use `84532` for Base Sepolia when pointing at a staging API.
|
|
|
199
217
|
|
|
200
218
|
If both `CONNECT_API_KEY` and `EVM_PRIVATE_KEY` are set, the API key is used.
|
|
201
219
|
|
|
202
|
-
|
|
220
|
+
Credentials are optional in local mode. Without either variable, lookup/scores/agent still work when the API does not charge x402. `connect_me` and Smart Send tools require `CONNECT_API_KEY`.
|
|
203
221
|
|
|
204
222
|
---
|
|
205
223
|
|
|
@@ -210,10 +228,13 @@ At least one of `CONNECT_API_KEY` or `EVM_PRIVATE_KEY` is required for local mod
|
|
|
210
228
|
| ---------------------------- | --------------------------------------------------------- |
|
|
211
229
|
| `connect_get_price` | List reference prices (no auth) |
|
|
212
230
|
| `connect_lookup` | Resolve social identities to EVM and SOL wallet addresses |
|
|
231
|
+
| `connect_lookup_exposed` | List platforms a recipient has exposed on Connect |
|
|
213
232
|
| `connect_scores_batch` | Batch scores for accounts or usernames |
|
|
214
233
|
| `connect_scores_by_account` | Scores for one linked account |
|
|
215
234
|
| `connect_scores_by_username` | Scores by Connect username |
|
|
235
|
+
| `connect_me` | API key owner profile, scores, and linked accounts — **API key only** |
|
|
216
236
|
| `connect_drop` | Smart Send (batch token transfer) — **API key only** |
|
|
237
|
+
| `connect_drop_balance` | Smart Send wallet balances on a chain — **API key only** |
|
|
217
238
|
| `connect_agent_prompt` | Natural-language agent for recipients discovery |
|
|
218
239
|
|
|
219
240
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AnonymousQuotaConfig } from './config.js';
|
|
2
|
+
export declare const ANON_QUOTA_EXEMPT_TOOLS: Set<string>;
|
|
3
|
+
export declare const ANON_AGENT_TOOL = "connect_agent_prompt";
|
|
4
|
+
export interface QuotaDecision {
|
|
5
|
+
allowed: boolean;
|
|
6
|
+
retryAfterSec?: number;
|
|
7
|
+
message?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class SlidingWindowLimiter {
|
|
10
|
+
private readonly limit;
|
|
11
|
+
private readonly windowMs;
|
|
12
|
+
private readonly now;
|
|
13
|
+
private hits;
|
|
14
|
+
constructor(limit: number, windowMs?: number, now?: () => number);
|
|
15
|
+
private prune;
|
|
16
|
+
wouldExceed(): {
|
|
17
|
+
exceeded: boolean;
|
|
18
|
+
retryAfterSec: number;
|
|
19
|
+
};
|
|
20
|
+
record(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare class AnonymousToolQuota {
|
|
23
|
+
private readonly global;
|
|
24
|
+
private readonly agent;
|
|
25
|
+
constructor(global: SlidingWindowLimiter, agent: SlidingWindowLimiter);
|
|
26
|
+
static fromConfig(config: AnonymousQuotaConfig, now?: () => number): AnonymousToolQuota;
|
|
27
|
+
consume(body: unknown): QuotaDecision;
|
|
28
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getJsonRpcMethod, getJsonRpcToolName } from './jsonrpc.js';
|
|
2
|
+
export const ANON_QUOTA_EXEMPT_TOOLS = new Set(['connect_get_price']);
|
|
3
|
+
export const ANON_AGENT_TOOL = 'connect_agent_prompt';
|
|
4
|
+
const GLOBAL_QUOTA_MESSAGE = 'Anonymous MCP quota exceeded. Get a Connect API key at https://connect.quid.li (Enable API access) for higher limits.';
|
|
5
|
+
const AGENT_QUOTA_MESSAGE = 'Anonymous MCP agent quota exceeded. Get a Connect API key at https://connect.quid.li (Enable API access) for higher limits.';
|
|
6
|
+
export class SlidingWindowLimiter {
|
|
7
|
+
limit;
|
|
8
|
+
windowMs;
|
|
9
|
+
now;
|
|
10
|
+
hits = [];
|
|
11
|
+
constructor(limit, windowMs = 60_000, now = Date.now) {
|
|
12
|
+
this.limit = limit;
|
|
13
|
+
this.windowMs = windowMs;
|
|
14
|
+
this.now = now;
|
|
15
|
+
}
|
|
16
|
+
prune(now) {
|
|
17
|
+
const cutoff = now - this.windowMs;
|
|
18
|
+
while (this.hits.length > 0 && this.hits[0] <= cutoff) {
|
|
19
|
+
this.hits.shift();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
wouldExceed() {
|
|
23
|
+
const now = this.now();
|
|
24
|
+
this.prune(now);
|
|
25
|
+
if (this.hits.length >= this.limit) {
|
|
26
|
+
const retryAfterSec = Math.max(1, Math.ceil((this.hits[0] + this.windowMs - now) / 1000));
|
|
27
|
+
return { exceeded: true, retryAfterSec };
|
|
28
|
+
}
|
|
29
|
+
return { exceeded: false, retryAfterSec: 0 };
|
|
30
|
+
}
|
|
31
|
+
record() {
|
|
32
|
+
this.hits.push(this.now());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class AnonymousToolQuota {
|
|
36
|
+
global;
|
|
37
|
+
agent;
|
|
38
|
+
constructor(global, agent) {
|
|
39
|
+
this.global = global;
|
|
40
|
+
this.agent = agent;
|
|
41
|
+
}
|
|
42
|
+
static fromConfig(config, now) {
|
|
43
|
+
return new AnonymousToolQuota(new SlidingWindowLimiter(config.rpm, 60_000, now), new SlidingWindowLimiter(config.agentRpm, 60_000, now));
|
|
44
|
+
}
|
|
45
|
+
consume(body) {
|
|
46
|
+
const method = getJsonRpcMethod(body);
|
|
47
|
+
if (method !== 'tools/call') {
|
|
48
|
+
return { allowed: true };
|
|
49
|
+
}
|
|
50
|
+
const toolName = getJsonRpcToolName(body);
|
|
51
|
+
if (toolName && ANON_QUOTA_EXEMPT_TOOLS.has(toolName)) {
|
|
52
|
+
return { allowed: true };
|
|
53
|
+
}
|
|
54
|
+
if (toolName === ANON_AGENT_TOOL) {
|
|
55
|
+
const agentLimit = this.agent.wouldExceed();
|
|
56
|
+
if (agentLimit.exceeded) {
|
|
57
|
+
return {
|
|
58
|
+
allowed: false,
|
|
59
|
+
retryAfterSec: agentLimit.retryAfterSec,
|
|
60
|
+
message: AGENT_QUOTA_MESSAGE,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const globalLimit = this.global.wouldExceed();
|
|
65
|
+
if (globalLimit.exceeded) {
|
|
66
|
+
return {
|
|
67
|
+
allowed: false,
|
|
68
|
+
retryAfterSec: globalLimit.retryAfterSec,
|
|
69
|
+
message: GLOBAL_QUOTA_MESSAGE,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (toolName === ANON_AGENT_TOOL) {
|
|
73
|
+
this.agent.record();
|
|
74
|
+
}
|
|
75
|
+
this.global.record();
|
|
76
|
+
return { allowed: true };
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface ConnectRequestOptions {
|
|
|
6
6
|
method: HttpMethod;
|
|
7
7
|
path: string;
|
|
8
8
|
authenticated?: boolean;
|
|
9
|
+
/** Fail before calling the API when CONNECT_API_KEY is unset (Smart Send /me). */
|
|
10
|
+
requireApiKey?: boolean;
|
|
9
11
|
body?: unknown;
|
|
10
12
|
query?: Record<string, string | undefined>;
|
|
11
13
|
timeoutMs?: number;
|
package/dist/client.js
CHANGED
|
@@ -7,7 +7,7 @@ export class ConnectClient {
|
|
|
7
7
|
paidFetch;
|
|
8
8
|
constructor(config) {
|
|
9
9
|
this.config = config;
|
|
10
|
-
this.authMode = config.apiKey ? '
|
|
10
|
+
this.authMode = config.evmPrivateKey && !config.apiKey ? 'x402' : 'api-key';
|
|
11
11
|
if (!config.apiKey && config.evmPrivateKey) {
|
|
12
12
|
this.paidFetch = createPaidFetch(config.evmPrivateKey, config.x402EvmNetwork);
|
|
13
13
|
}
|
|
@@ -21,6 +21,9 @@ export class ConnectClient {
|
|
|
21
21
|
async request(options) {
|
|
22
22
|
const { method, path, body, query, timeoutMs } = options;
|
|
23
23
|
const authenticated = options.authenticated ?? true;
|
|
24
|
+
if (options.requireApiKey && !this.config.apiKey) {
|
|
25
|
+
return errorResult('This tool requires CONNECT_API_KEY. Get a key at https://connect.quid.li (Enable API access). x402 wallet auth is not supported for this action.');
|
|
26
|
+
}
|
|
24
27
|
const url = new URL(path, `${this.config.baseUrl}/`);
|
|
25
28
|
if (query) {
|
|
26
29
|
for (const [key, value] of Object.entries(query)) {
|
package/dist/config.d.ts
CHANGED
|
@@ -13,7 +13,18 @@ export interface HttpServerConfig {
|
|
|
13
13
|
baseUrl: string;
|
|
14
14
|
}
|
|
15
15
|
export declare function loadHttpServerConfig(env?: NodeJS.ProcessEnv): HttpServerConfig;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Stdio MCP: credentials are optional.
|
|
18
|
+
* Lookup/scores/agent work without auth when the API allows it (x402 price 0)
|
|
19
|
+
* or with CONNECT_API_KEY / EVM_PRIVATE_KEY. /account/me and /drop require an API key.
|
|
20
|
+
*/
|
|
17
21
|
export declare function loadConfig(env?: NodeJS.ProcessEnv): ConnectClientConfig;
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
export interface AnonymousQuotaConfig {
|
|
23
|
+
allowAnonymous: boolean;
|
|
24
|
+
rpm: number;
|
|
25
|
+
agentRpm: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function isAnonymousMcpAllowed(env?: NodeJS.ProcessEnv): boolean;
|
|
28
|
+
export declare function loadAnonymousQuotaConfig(env?: NodeJS.ProcessEnv): AnonymousQuotaConfig;
|
|
29
|
+
/** Hosted HTTP MCP: api-key from request header when present. */
|
|
30
|
+
export declare function createHostedClientConfig(apiKey: string | undefined, baseUrl: string): ConnectClientConfig;
|
package/dist/config.js
CHANGED
|
@@ -23,14 +23,15 @@ export function loadHttpServerConfig(env = process.env) {
|
|
|
23
23
|
}
|
|
24
24
|
return { host, port, baseUrl: loadBaseUrl(env) };
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Stdio MCP: credentials are optional.
|
|
28
|
+
* Lookup/scores/agent work without auth when the API allows it (x402 price 0)
|
|
29
|
+
* or with CONNECT_API_KEY / EVM_PRIVATE_KEY. /account/me and /drop require an API key.
|
|
30
|
+
*/
|
|
27
31
|
export function loadConfig(env = process.env) {
|
|
28
32
|
const apiKey = env.CONNECT_API_KEY?.trim() || undefined;
|
|
29
33
|
const evmPrivateKey = normalizeEvmPrivateKey(env.EVM_PRIVATE_KEY);
|
|
30
34
|
const x402EvmNetwork = parseX402EvmNetwork(env);
|
|
31
|
-
if (!apiKey && !evmPrivateKey) {
|
|
32
|
-
throw new Error('CONNECT_API_KEY or EVM_PRIVATE_KEY is required. Get an API key at https://connect.quid.li (Enable API access), or set EVM_PRIVATE_KEY for pay-per-call x402.');
|
|
33
|
-
}
|
|
34
35
|
return {
|
|
35
36
|
baseUrl: loadBaseUrl(env),
|
|
36
37
|
apiKey,
|
|
@@ -38,11 +39,35 @@ export function loadConfig(env = process.env) {
|
|
|
38
39
|
x402EvmNetwork,
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
const DEFAULT_ANON_RPM = 50;
|
|
43
|
+
const DEFAULT_ANON_AGENT_RPM = 5;
|
|
44
|
+
export function isAnonymousMcpAllowed(env = process.env) {
|
|
45
|
+
const raw = env.CONNECT_MCP_ALLOW_ANONYMOUS?.trim().toLowerCase();
|
|
46
|
+
return raw === 'true' || raw === '1';
|
|
47
|
+
}
|
|
48
|
+
function parsePositiveInt(raw, fallback, name) {
|
|
49
|
+
if (!raw?.trim()) {
|
|
50
|
+
return fallback;
|
|
51
|
+
}
|
|
52
|
+
const value = Number(raw);
|
|
53
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
54
|
+
throw new Error(`${name} must be a positive integer (got "${raw}")`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
export function loadAnonymousQuotaConfig(env = process.env) {
|
|
59
|
+
return {
|
|
60
|
+
allowAnonymous: isAnonymousMcpAllowed(env),
|
|
61
|
+
rpm: parsePositiveInt(env.CONNECT_MCP_ANON_RPM, DEFAULT_ANON_RPM, 'CONNECT_MCP_ANON_RPM'),
|
|
62
|
+
agentRpm: parsePositiveInt(env.CONNECT_MCP_ANON_AGENT_RPM, DEFAULT_ANON_AGENT_RPM, 'CONNECT_MCP_ANON_AGENT_RPM'),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Hosted HTTP MCP: api-key from request header when present. */
|
|
42
66
|
export function createHostedClientConfig(apiKey, baseUrl) {
|
|
67
|
+
const trimmed = apiKey?.trim() || undefined;
|
|
43
68
|
return {
|
|
44
69
|
baseUrl,
|
|
45
|
-
apiKey,
|
|
70
|
+
apiKey: trimmed,
|
|
46
71
|
x402EvmNetwork: DEFAULT_X402_EVM_NETWORK,
|
|
47
72
|
};
|
|
48
73
|
}
|
package/dist/deeplink.d.ts
CHANGED
package/dist/deeplink.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { CONNECT_MCP_API_KEY_PLACEHOLDER, CONNECT_MCP_REMOTE_URL } from './metadata.js';
|
|
2
2
|
export function buildCursorRemoteMcpConfig(apiKey = CONNECT_MCP_API_KEY_PLACEHOLDER) {
|
|
3
|
+
const trimmed = apiKey.trim();
|
|
4
|
+
if (!trimmed) {
|
|
5
|
+
return { url: CONNECT_MCP_REMOTE_URL };
|
|
6
|
+
}
|
|
3
7
|
return {
|
|
4
8
|
url: CONNECT_MCP_REMOTE_URL,
|
|
5
9
|
headers: {
|
|
6
|
-
'x-api-key':
|
|
10
|
+
'x-api-key': trimmed,
|
|
7
11
|
},
|
|
8
12
|
};
|
|
9
13
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AnonymousToolQuota } from './anonymous-quota.js';
|
|
2
|
+
import { type HeaderCarrier } from './http-auth.js';
|
|
3
|
+
export interface HostedMcpGateOk {
|
|
4
|
+
ok: true;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface HostedMcpGateError {
|
|
8
|
+
ok: false;
|
|
9
|
+
status: number;
|
|
10
|
+
message: string;
|
|
11
|
+
code: number;
|
|
12
|
+
retryAfterSec?: number;
|
|
13
|
+
}
|
|
14
|
+
export type HostedMcpGateResult = HostedMcpGateOk | HostedMcpGateError;
|
|
15
|
+
export interface HostedMcpGateRequest extends HeaderCarrier {
|
|
16
|
+
body?: unknown;
|
|
17
|
+
}
|
|
18
|
+
export declare function gateHostedMcpRequest(req: HostedMcpGateRequest, options: {
|
|
19
|
+
env?: NodeJS.ProcessEnv;
|
|
20
|
+
anonymousQuota: AnonymousToolQuota;
|
|
21
|
+
}): HostedMcpGateResult;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { validateRequestApiKey } from './http-auth.js';
|
|
2
|
+
export function gateHostedMcpRequest(req, options) {
|
|
3
|
+
const env = options.env ?? process.env;
|
|
4
|
+
const auth = validateRequestApiKey(req, env);
|
|
5
|
+
if (!auth.ok) {
|
|
6
|
+
return { ok: false, status: 401, message: auth.message, code: -32001 };
|
|
7
|
+
}
|
|
8
|
+
if (!auth.apiKey) {
|
|
9
|
+
const quota = options.anonymousQuota.consume(req.body);
|
|
10
|
+
if (!quota.allowed) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
status: 429,
|
|
14
|
+
message: quota.message ?? 'Anonymous MCP quota exceeded.',
|
|
15
|
+
code: -32029,
|
|
16
|
+
retryAfterSec: quota.retryAfterSec,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { ok: true, apiKey: auth.apiKey };
|
|
21
|
+
}
|
package/dist/http-auth.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IncomingHttpHeaders } from 'node:http';
|
|
2
2
|
export interface RequestApiKeyResult {
|
|
3
3
|
ok: true;
|
|
4
|
-
apiKey
|
|
4
|
+
apiKey?: string;
|
|
5
5
|
}
|
|
6
6
|
export interface RequestApiKeyError {
|
|
7
7
|
ok: false;
|
|
8
8
|
message: string;
|
|
9
9
|
}
|
|
10
10
|
export type RequestApiKeyValidation = RequestApiKeyResult | RequestApiKeyError;
|
|
11
|
-
export
|
|
11
|
+
export interface HeaderCarrier {
|
|
12
|
+
headers: IncomingHttpHeaders;
|
|
13
|
+
}
|
|
14
|
+
export declare function extractRequestApiKey(req: HeaderCarrier): string | undefined;
|
|
12
15
|
export declare function isPlaceholderApiKey(apiKey: string): boolean;
|
|
13
|
-
export declare function validateRequestApiKey(req:
|
|
16
|
+
export declare function validateRequestApiKey(req: HeaderCarrier, env?: NodeJS.ProcessEnv): RequestApiKeyValidation;
|
package/dist/http-auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAnonymousMcpAllowed } from './config.js';
|
|
1
2
|
import { CONNECT_MCP_API_KEY_PLACEHOLDER } from './metadata.js';
|
|
2
3
|
const PLACEHOLDER_API_KEYS = new Set([
|
|
3
4
|
'your-connect-api-key',
|
|
@@ -14,9 +15,12 @@ export function extractRequestApiKey(req) {
|
|
|
14
15
|
export function isPlaceholderApiKey(apiKey) {
|
|
15
16
|
return PLACEHOLDER_API_KEYS.has(apiKey);
|
|
16
17
|
}
|
|
17
|
-
export function validateRequestApiKey(req) {
|
|
18
|
+
export function validateRequestApiKey(req, env = process.env) {
|
|
18
19
|
const apiKey = extractRequestApiKey(req);
|
|
19
20
|
if (!apiKey) {
|
|
21
|
+
if (isAnonymousMcpAllowed(env)) {
|
|
22
|
+
return { ok: true };
|
|
23
|
+
}
|
|
20
24
|
return {
|
|
21
25
|
ok: false,
|
|
22
26
|
message: 'Missing x-api-key header. Get a key at https://connect.quid.li (Enable API access) and set it in your MCP headers.',
|
package/dist/index-http.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/jsonrpc.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function getJsonRpcMethod(body) {
|
|
2
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
const method = body.method;
|
|
6
|
+
return typeof method === 'string' ? method : undefined;
|
|
7
|
+
}
|
|
8
|
+
export function getJsonRpcToolName(body) {
|
|
9
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const params = body.params;
|
|
13
|
+
return typeof params?.name === 'string' ? params.name : undefined;
|
|
14
|
+
}
|
package/dist/metadata.js
CHANGED
package/dist/mount.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { IRouter } from 'express';
|
|
2
|
+
import { AnonymousToolQuota } from './anonymous-quota.js';
|
|
2
3
|
export { resolveRequestHost } from './request-host.js';
|
|
3
4
|
export interface ConnectMcpMountOptions {
|
|
4
5
|
baseUrl: string;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
anonymousQuota?: AnonymousToolQuota;
|
|
5
8
|
}
|
|
6
9
|
export declare function resolveEmbeddedApiBaseUrl(port: number, env?: NodeJS.ProcessEnv): string;
|
|
7
10
|
export declare function isConnectMcpHost(hostname: string, env?: NodeJS.ProcessEnv): boolean;
|
package/dist/mount.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
2
|
-
import {
|
|
2
|
+
import { AnonymousToolQuota } from './anonymous-quota.js';
|
|
3
|
+
import { createHostedClientConfig, loadAnonymousQuotaConfig } from './config.js';
|
|
3
4
|
import { createMcpServer } from './create-mcp-server.js';
|
|
4
|
-
import {
|
|
5
|
+
import { gateHostedMcpRequest } from './hosted-gate.js';
|
|
5
6
|
export { resolveRequestHost } from './request-host.js';
|
|
6
|
-
function sendJsonRpcError(res, status, message) {
|
|
7
|
+
function sendJsonRpcError(res, status, message, options) {
|
|
7
8
|
if (res.headersSent) {
|
|
8
9
|
return;
|
|
9
10
|
}
|
|
11
|
+
if (options?.retryAfterSec) {
|
|
12
|
+
res.setHeader('Retry-After', String(options.retryAfterSec));
|
|
13
|
+
}
|
|
10
14
|
res.status(status).json({
|
|
11
15
|
jsonrpc: '2.0',
|
|
12
16
|
error: {
|
|
13
|
-
code: -32001,
|
|
17
|
+
code: options?.code ?? -32001,
|
|
14
18
|
message,
|
|
15
19
|
},
|
|
16
20
|
id: null,
|
|
@@ -37,6 +41,8 @@ export function isConnectMcpHost(hostname, env = process.env) {
|
|
|
37
41
|
}
|
|
38
42
|
export function mountConnectMcpHttp(router, options) {
|
|
39
43
|
const { baseUrl } = options;
|
|
44
|
+
const env = options.env ?? process.env;
|
|
45
|
+
const anonymousQuota = options.anonymousQuota ?? AnonymousToolQuota.fromConfig(loadAnonymousQuotaConfig(env));
|
|
40
46
|
router.get('/health', (_req, res) => {
|
|
41
47
|
res.json({ ok: true, service: 'quidli-connect-mcp' });
|
|
42
48
|
});
|
|
@@ -44,13 +50,16 @@ export function mountConnectMcpHttp(router, options) {
|
|
|
44
50
|
// No in-memory session map, so Cloud Run restarts / multiple instances
|
|
45
51
|
// never cause "Missing or invalid MCP session" errors.
|
|
46
52
|
router.all('/', async (req, res) => {
|
|
47
|
-
const
|
|
48
|
-
if (!
|
|
49
|
-
sendJsonRpcError(res,
|
|
53
|
+
const gate = gateHostedMcpRequest(req, { env, anonymousQuota });
|
|
54
|
+
if (!gate.ok) {
|
|
55
|
+
sendJsonRpcError(res, gate.status, gate.message, {
|
|
56
|
+
code: gate.code,
|
|
57
|
+
retryAfterSec: gate.retryAfterSec,
|
|
58
|
+
});
|
|
50
59
|
return;
|
|
51
60
|
}
|
|
52
61
|
try {
|
|
53
|
-
const server = createMcpServer(createHostedClientConfig(
|
|
62
|
+
const server = createMcpServer(createHostedClientConfig(gate.apiKey, baseUrl));
|
|
54
63
|
const transport = new StreamableHTTPServerTransport({
|
|
55
64
|
sessionIdGenerator: undefined, // stateless — no session tracking
|
|
56
65
|
enableJsonResponse: true,
|
package/dist/schemas.d.ts
CHANGED
|
@@ -28,6 +28,21 @@ export declare const lookupInputSchema: {
|
|
|
28
28
|
username: z.ZodOptional<z.ZodString>;
|
|
29
29
|
}, z.ZodTypeAny, "passthrough">>, "many">;
|
|
30
30
|
};
|
|
31
|
+
export declare const lookupExposedInputSchema: {
|
|
32
|
+
recipient: z.ZodObject<{
|
|
33
|
+
type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
|
|
34
|
+
id: z.ZodOptional<z.ZodString>;
|
|
35
|
+
username: z.ZodOptional<z.ZodString>;
|
|
36
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
37
|
+
type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
|
|
38
|
+
id: z.ZodOptional<z.ZodString>;
|
|
39
|
+
username: z.ZodOptional<z.ZodString>;
|
|
40
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
41
|
+
type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
|
|
42
|
+
id: z.ZodOptional<z.ZodString>;
|
|
43
|
+
username: z.ZodOptional<z.ZodString>;
|
|
44
|
+
}, z.ZodTypeAny, "passthrough">>;
|
|
45
|
+
};
|
|
31
46
|
export declare const scoresBatchInputSchema: {
|
|
32
47
|
users: z.ZodArray<z.ZodObject<{
|
|
33
48
|
type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
|
|
@@ -60,6 +75,9 @@ export declare const scoresByAccountInputSchema: {
|
|
|
60
75
|
export declare const scoresByUsernameInputSchema: {
|
|
61
76
|
username: z.ZodString;
|
|
62
77
|
};
|
|
78
|
+
export declare const dropBalanceInputSchema: {
|
|
79
|
+
chainId: z.ZodNumber;
|
|
80
|
+
};
|
|
63
81
|
export declare const dropInputSchema: {
|
|
64
82
|
idempotencyKey: z.ZodString;
|
|
65
83
|
chainId: z.ZodNumber;
|
package/dist/schemas.js
CHANGED
|
@@ -26,6 +26,9 @@ export const lookupInputSchema = {
|
|
|
26
26
|
.min(1)
|
|
27
27
|
.describe('Recipients to resolve to EVM/Solana wallets'),
|
|
28
28
|
};
|
|
29
|
+
export const lookupExposedInputSchema = {
|
|
30
|
+
recipient: linkedAccountSchema.describe('Recipient whose exposed linked accounts and Connect profile should be listed'),
|
|
31
|
+
};
|
|
29
32
|
export const scoresBatchInputSchema = {
|
|
30
33
|
users: z.array(linkedAccountSchema).min(1).describe('Users to fetch scores for'),
|
|
31
34
|
filter: z
|
|
@@ -59,6 +62,9 @@ export const scoresByAccountInputSchema = {
|
|
|
59
62
|
export const scoresByUsernameInputSchema = {
|
|
60
63
|
username: z.string().min(1),
|
|
61
64
|
};
|
|
65
|
+
export const dropBalanceInputSchema = {
|
|
66
|
+
chainId: z.number().int().describe('EVM chain ID (Smart Send supported chains only)'),
|
|
67
|
+
};
|
|
62
68
|
export const dropInputSchema = {
|
|
63
69
|
idempotencyKey: z.string().uuid(),
|
|
64
70
|
chainId: z.number().int(),
|
package/dist/tools/register.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { ConnectClient } from '../client.js';
|
|
3
|
-
export declare const CONNECT_MCP_TOOL_NAMES: readonly ["connect_get_price", "connect_lookup", "connect_scores_batch", "connect_scores_by_account", "connect_scores_by_username", "connect_drop", "connect_agent_prompt"];
|
|
3
|
+
export declare const CONNECT_MCP_TOOL_NAMES: readonly ["connect_get_price", "connect_lookup", "connect_lookup_exposed", "connect_scores_batch", "connect_scores_by_account", "connect_scores_by_username", "connect_me", "connect_drop", "connect_drop_balance", "connect_agent_prompt"];
|
|
4
4
|
export declare function registerTools(server: McpServer, client: ConnectClient): void;
|
package/dist/tools/register.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { agentPromptInputSchema, dropInputSchema, lookupInputSchema, scoresBatchInputSchema, scoresByAccountInputSchema, scoresByUsernameInputSchema, } from '../schemas.js';
|
|
1
|
+
import { agentPromptInputSchema, dropBalanceInputSchema, dropInputSchema, lookupExposedInputSchema, lookupInputSchema, scoresBatchInputSchema, scoresByAccountInputSchema, scoresByUsernameInputSchema, } from '../schemas.js';
|
|
2
2
|
const AGENT_TIMEOUT_MS = 65_000;
|
|
3
3
|
export const CONNECT_MCP_TOOL_NAMES = [
|
|
4
4
|
'connect_get_price',
|
|
5
5
|
'connect_lookup',
|
|
6
|
+
'connect_lookup_exposed',
|
|
6
7
|
'connect_scores_batch',
|
|
7
8
|
'connect_scores_by_account',
|
|
8
9
|
'connect_scores_by_username',
|
|
10
|
+
'connect_me',
|
|
9
11
|
'connect_drop',
|
|
12
|
+
'connect_drop_balance',
|
|
10
13
|
'connect_agent_prompt',
|
|
11
14
|
];
|
|
12
15
|
export function registerTools(server, client) {
|
|
@@ -16,6 +19,11 @@ export function registerTools(server, client) {
|
|
|
16
19
|
path: '/lookup',
|
|
17
20
|
body: { recipients },
|
|
18
21
|
}));
|
|
22
|
+
server.tool('connect_lookup_exposed', 'List platforms a recipient has exposed on Connect, with enriched profile, scores, and wallet addresses. May require x402 payment when the profile owner charges for lookups.', lookupExposedInputSchema, async ({ recipient }) => client.request({
|
|
23
|
+
method: 'POST',
|
|
24
|
+
path: '/lookup/exposed',
|
|
25
|
+
body: { recipient },
|
|
26
|
+
}));
|
|
19
27
|
server.tool('connect_scores_batch', 'Batch scores for linked accounts or Connect usernames. Optional filter excludes users below minScore.', scoresBatchInputSchema, async ({ users, filter }) => client.request({
|
|
20
28
|
method: 'POST',
|
|
21
29
|
path: '/scores',
|
|
@@ -29,14 +37,25 @@ export function registerTools(server, client) {
|
|
|
29
37
|
method: 'GET',
|
|
30
38
|
path: `/scores/u/${encodeURIComponent(username.trim())}`,
|
|
31
39
|
}));
|
|
40
|
+
server.tool('connect_me', 'Get the Connect profile, scores, and all linked accounts for the API key owner. Use to identify which user the key belongs to.', {}, async () => client.request({
|
|
41
|
+
method: 'GET',
|
|
42
|
+
path: '/account/me',
|
|
43
|
+
}));
|
|
32
44
|
server.tool('connect_drop', 'Execute a Smart Send (batch token transfer). Returns 201 when submitted or 202 when recipients still processing — retry with same idempotencyKey.', dropInputSchema, async ({ ignoreFailedRecipients, ...body }) => client.request({
|
|
33
45
|
method: 'POST',
|
|
34
46
|
path: '/drop',
|
|
35
47
|
body,
|
|
48
|
+
requireApiKey: true,
|
|
36
49
|
query: ignoreFailedRecipients === true
|
|
37
50
|
? { ignoreFailedRecipients: 'true' }
|
|
38
51
|
: undefined,
|
|
39
52
|
}));
|
|
53
|
+
server.tool('connect_drop_balance', 'Get native and ERC-20 balances for the API key owner Smart Send embedded wallet on a chain. Use before connect_drop to verify gas and token funds.', dropBalanceInputSchema, async ({ chainId }) => client.request({
|
|
54
|
+
method: 'GET',
|
|
55
|
+
path: '/drop/balance',
|
|
56
|
+
requireApiKey: true,
|
|
57
|
+
query: { chainId: String(chainId) },
|
|
58
|
+
}));
|
|
40
59
|
server.tool('connect_agent_prompt', 'Natural-language agent turn for Farcaster/Lens cohort discovery. Blocking up to 60s. Omit sessionId to start; include to continue. Do not send requester.', agentPromptInputSchema, async ({ prompt, sessionId }) => client.request({
|
|
41
60
|
method: 'POST',
|
|
42
61
|
path: '/agent',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quidli/connect-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "MCP server for Quidli Connect product APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"./http": "./dist/http.js"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"build": "tsc
|
|
43
|
-
"prepublishOnly": "npm run build",
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"prepublishOnly": "npm run build && chmod +x dist/index.js dist/index-http.js",
|
|
44
44
|
"start": "node dist/index.js",
|
|
45
45
|
"start:http": "node dist/index-http.js",
|
|
46
46
|
"test": "vitest run",
|