@quidli/connect-mcp 0.1.1 → 0.3.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 +187 -61
- package/dist/client.d.ts +6 -0
- package/dist/client.js +21 -4
- package/dist/config.d.ts +13 -1
- package/dist/config.js +36 -8
- package/dist/deeplink.js +1 -1
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +7 -3
- package/dist/index-http.js +0 -0
- package/dist/index.js +0 -0
- package/dist/mount.js +2 -1
- 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/dist/x402-fetch.d.ts +2 -0
- package/dist/x402-fetch.js +30 -0
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -1,57 +1,128 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Quidli Connect MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use [Quidli Connect](https://connect.quid.li) from Cursor, Claude Desktop, or any MCP-compatible client — wallet lookup, scores, Smart Send drops, and the Connect agent.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Choose how to connect
|
|
6
6
|
|
|
7
|
-
- Node.js 20+
|
|
8
|
-
- A Connect API key ([Enable API access](https://connect.quid.li))
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
| | **Hosted** (`mcp.connect.quid.li`) | **Local** (`npx @quidli/connect-mcp`) |
|
|
9
|
+
| --------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
10
|
+
| **Setup** | Add a URL in MCP settings | Run a local process via `npx` |
|
|
11
|
+
| **Auth** | Connect API key only | API key, x402 pay-per-call, or none for lookup/scores/agent |
|
|
12
|
+
| **Pros** | No install; always on Quidli infrastructure; simplest setup | Wallet private key never leaves your machine; pay per call with USDC instead of an API key |
|
|
13
|
+
| **Cons** | Requires a Connect API key; no pay-per-call billing | Requires Node.js 20+; `npx` may download the package on first run |
|
|
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 |
|
|
11
15
|
|
|
12
|
-
### Remote URL (recommended for production)
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
Get a Connect API key at [connect.quid.li](https://connect.quid.li) → **Enable API access**.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Hosted (recommended)
|
|
22
|
+
|
|
23
|
+
Best if you have a Connect API key and want zero local setup.
|
|
24
|
+
|
|
25
|
+
### Cursor
|
|
26
|
+
|
|
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
|
+
|
|
29
|
+
[Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=quidli-connect&config=eyJ1cmwiOiJodHRwczovL21jcC5jb25uZWN0LnF1aWQubGkiLCJoZWFkZXJzIjp7IngtYXBpLWtleSI6IiJ9fQ==)
|
|
30
|
+
|
|
31
|
+
Or paste into **Cursor Settings → MCP → Add new MCP server**:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
cursor://anysphere.cursor-deeplink/mcp/install?name=quidli-connect&config=eyJ1cmwiOiJodHRwczovL21jcC5jb25uZWN0LnF1aWQubGkiLCJoZWFkZXJzIjp7IngtYXBpLWtleSI6IiJ9fQ==
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then open **Cursor Settings → MCP → quidli-connect** and set `headers.x-api-key` to your Connect API key.
|
|
38
|
+
|
|
39
|
+
#### Add manually {#cursor-manual}
|
|
40
|
+
|
|
41
|
+
Merge this into `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project) under `mcpServers` — do not replace your existing entries:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
"quidli-connect": {
|
|
45
|
+
"url": "https://mcp.connect.quid.li",
|
|
46
|
+
"headers": {
|
|
47
|
+
"x-api-key": "<your-connect-api-key>"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Claude Desktop
|
|
53
|
+
|
|
54
|
+
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` and header `x-api-key: <your-connect-api-key>`.
|
|
56
|
+
|
|
57
|
+
Or merge this into your config file and restart Claude:
|
|
58
|
+
|
|
59
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
60
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
15
61
|
|
|
16
62
|
```json
|
|
17
63
|
{
|
|
18
64
|
"mcpServers": {
|
|
19
|
-
"connect": {
|
|
65
|
+
"quidli-connect": {
|
|
20
66
|
"url": "https://mcp.connect.quid.li",
|
|
21
67
|
"headers": {
|
|
22
|
-
"x-api-key": "
|
|
68
|
+
"x-api-key": "<your-connect-api-key>"
|
|
23
69
|
}
|
|
24
70
|
}
|
|
25
71
|
}
|
|
26
72
|
}
|
|
27
73
|
```
|
|
28
74
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
**Add to Cursor** deeplink (placeholder key pre-filled):
|
|
75
|
+
If your Claude version does not support remote `url` connectors, use the local bridge instead (still hosted API, runs a small local helper):
|
|
32
76
|
|
|
33
|
-
```
|
|
34
|
-
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"quidli-connect": {
|
|
81
|
+
"command": "npx",
|
|
82
|
+
"args": [
|
|
83
|
+
"-y",
|
|
84
|
+
"mcp-remote@latest",
|
|
85
|
+
"--http",
|
|
86
|
+
"https://mcp.connect.quid.li",
|
|
87
|
+
"--header",
|
|
88
|
+
"x-api-key:${CONNECT_API_KEY}"
|
|
89
|
+
],
|
|
90
|
+
"env": {
|
|
91
|
+
"CONNECT_API_KEY": "<your-connect-api-key>"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
35
96
|
```
|
|
36
97
|
|
|
37
|
-
|
|
98
|
+
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.
|
|
38
99
|
|
|
39
|
-
|
|
40
|
-
import { buildCursorMcpInstallDeeplink } from '@quidli/connect-mcp/deeplink';
|
|
41
|
-
```
|
|
100
|
+
Until a valid API key is set, requests return `401` with setup instructions.
|
|
42
101
|
|
|
43
|
-
|
|
102
|
+
---
|
|
44
103
|
|
|
45
|
-
|
|
104
|
+
## Local (stdio)
|
|
105
|
+
|
|
106
|
+
Best if you want pay-per-call with a wallet, or prefer credentials in a local env file.
|
|
107
|
+
|
|
108
|
+
Requires **Node.js 20+**.
|
|
109
|
+
|
|
110
|
+
### Cursor
|
|
111
|
+
|
|
112
|
+
Add one of the following to `.cursor/mcp.json` (project) or your global MCP settings.
|
|
113
|
+
|
|
114
|
+
#### Option A — API key
|
|
115
|
+
|
|
116
|
+
Same billing model as hosted; credentials stay on your machine.
|
|
46
117
|
|
|
47
118
|
```json
|
|
48
119
|
{
|
|
49
120
|
"mcpServers": {
|
|
50
|
-
"connect": {
|
|
121
|
+
"quidli-connect": {
|
|
51
122
|
"command": "npx",
|
|
52
123
|
"args": ["-y", "@quidli/connect-mcp"],
|
|
53
124
|
"env": {
|
|
54
|
-
"CONNECT_API_KEY": "<your-api-key>",
|
|
125
|
+
"CONNECT_API_KEY": "<your-connect-api-key>",
|
|
55
126
|
"CONNECT_API_BASE_URL": "https://api.connect.quid.li"
|
|
56
127
|
}
|
|
57
128
|
}
|
|
@@ -59,59 +130,114 @@ Add to `.cursor/mcp.json` (or global MCP settings):
|
|
|
59
130
|
}
|
|
60
131
|
```
|
|
61
132
|
|
|
62
|
-
|
|
133
|
+
#### Option B — x402 pay-per-call (wallet)
|
|
63
134
|
|
|
64
|
-
|
|
135
|
+
No API key. Authenticated calls pay automatically when the API returns HTTP 402. Your wallet private key is only read by the local process.
|
|
65
136
|
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"mcpServers": {
|
|
140
|
+
"quidli-connect": {
|
|
141
|
+
"command": "npx",
|
|
142
|
+
"args": ["-y", "@quidli/connect-mcp"],
|
|
143
|
+
"env": {
|
|
144
|
+
"EVM_PRIVATE_KEY": "0x<wallet-with-usdc-on-base>",
|
|
145
|
+
"CONNECT_API_BASE_URL": "https://api.connect.quid.li",
|
|
146
|
+
"CONNECT_X402_EVM_NETWORK": "8453"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
70
151
|
```
|
|
71
152
|
|
|
72
|
-
|
|
153
|
+
Use `84532` for Base Sepolia when pointing at a staging API.
|
|
73
154
|
|
|
74
|
-
|
|
75
|
-
|----------|---------|-------------|
|
|
76
|
-
| `CONNECT_MCP_HTTP_HOST` | `0.0.0.0` | Bind address |
|
|
77
|
-
| `CONNECT_MCP_HTTP_PORT` | `8080` | Listen port |
|
|
78
|
-
| `CONNECT_API_BASE_URL` | `https://api.connect.quid.li` | Upstream Connect API |
|
|
155
|
+
#### Option C — no credentials (lookup / scores / agent)
|
|
79
156
|
|
|
80
|
-
|
|
157
|
+
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.
|
|
81
158
|
|
|
82
|
-
|
|
159
|
+
`connect_me`, `connect_drop`, and `connect_drop_balance` still require `CONNECT_API_KEY`.
|
|
83
160
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"mcpServers": {
|
|
164
|
+
"quidli-connect": {
|
|
165
|
+
"command": "npx",
|
|
166
|
+
"args": ["-y", "@quidli/connect-mcp"],
|
|
167
|
+
"env": {
|
|
168
|
+
"CONNECT_API_BASE_URL": "http://127.0.0.1:3011"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Claude Desktop
|
|
93
176
|
|
|
94
|
-
|
|
177
|
+
1. Open **Claude → Settings → Developer → Edit Config** (enable Developer mode first if you do not see it).
|
|
178
|
+
2. Merge one of the blocks below into `mcpServers` in:
|
|
95
179
|
|
|
96
|
-
|
|
180
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
181
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
97
182
|
|
|
98
|
-
|
|
99
|
-
- `connect_get_price` is public on the HTTP API and does not send `x-api-key`.
|
|
183
|
+
3. Restart Claude Desktop.
|
|
100
184
|
|
|
101
|
-
|
|
185
|
+
#### Option A — API key
|
|
102
186
|
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"mcpServers": {
|
|
190
|
+
"quidli-connect": {
|
|
191
|
+
"command": "npx",
|
|
192
|
+
"args": ["-y", "@quidli/connect-mcp"],
|
|
193
|
+
"env": {
|
|
194
|
+
"CONNECT_API_KEY": "<your-connect-api-key>",
|
|
195
|
+
"CONNECT_API_BASE_URL": "https://api.connect.quid.li"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
109
200
|
```
|
|
110
201
|
|
|
111
|
-
|
|
202
|
+
#### Option B — x402 pay-per-call (wallet)
|
|
112
203
|
|
|
113
|
-
```
|
|
114
|
-
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"mcpServers": {
|
|
207
|
+
"quidli-connect": {
|
|
208
|
+
"command": "npx",
|
|
209
|
+
"args": ["-y", "@quidli/connect-mcp"],
|
|
210
|
+
"env": {
|
|
211
|
+
"EVM_PRIVATE_KEY": "0x<wallet-with-usdc-on-base>",
|
|
212
|
+
"CONNECT_API_BASE_URL": "https://api.connect.quid.li",
|
|
213
|
+
"CONNECT_X402_EVM_NETWORK": "8453"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
115
218
|
```
|
|
116
219
|
|
|
117
|
-
|
|
220
|
+
If both `CONNECT_API_KEY` and `EVM_PRIVATE_KEY` are set, the API key is used.
|
|
221
|
+
|
|
222
|
+
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`.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Tools
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
| Tool | What it does |
|
|
230
|
+
| ---------------------------- | --------------------------------------------------------- |
|
|
231
|
+
| `connect_get_price` | List reference prices (no auth) |
|
|
232
|
+
| `connect_lookup` | Resolve social identities to EVM and SOL wallet addresses |
|
|
233
|
+
| `connect_lookup_exposed` | List platforms a recipient has exposed on Connect |
|
|
234
|
+
| `connect_scores_batch` | Batch scores for accounts or usernames |
|
|
235
|
+
| `connect_scores_by_account` | Scores for one linked account |
|
|
236
|
+
| `connect_scores_by_username` | Scores by Connect username |
|
|
237
|
+
| `connect_me` | API key owner profile, scores, and linked accounts — **API key only** |
|
|
238
|
+
| `connect_drop` | Smart Send (batch token transfer) — **API key only** |
|
|
239
|
+
| `connect_drop_balance` | Smart Send wallet balances on a chain — **API key only** |
|
|
240
|
+
| `connect_agent_prompt` | Natural-language agent for recipients discovery |
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
Ask your client to use these tools when you need Connect data or actions.
|
package/dist/client.d.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import type { ConnectClientConfig } from './config.js';
|
|
2
2
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
export type HttpMethod = 'GET' | 'POST';
|
|
4
|
+
export type ConnectAuthMode = 'api-key' | 'x402';
|
|
4
5
|
export interface ConnectRequestOptions {
|
|
5
6
|
method: HttpMethod;
|
|
6
7
|
path: string;
|
|
7
8
|
authenticated?: boolean;
|
|
9
|
+
/** Fail before calling the API when CONNECT_API_KEY is unset (Smart Send /me). */
|
|
10
|
+
requireApiKey?: boolean;
|
|
8
11
|
body?: unknown;
|
|
9
12
|
query?: Record<string, string | undefined>;
|
|
10
13
|
timeoutMs?: number;
|
|
11
14
|
}
|
|
12
15
|
export declare class ConnectClient {
|
|
13
16
|
private readonly config;
|
|
17
|
+
private readonly authMode;
|
|
18
|
+
private readonly paidFetch?;
|
|
14
19
|
constructor(config: ConnectClientConfig);
|
|
20
|
+
private resolveFetch;
|
|
15
21
|
request(options: ConnectRequestOptions): Promise<CallToolResult>;
|
|
16
22
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import { mapHttpError } from './errors.js';
|
|
2
2
|
import { errorResult, successResult } from './result.js';
|
|
3
|
+
import { createPaidFetch } from './x402-fetch.js';
|
|
3
4
|
export class ConnectClient {
|
|
4
5
|
config;
|
|
6
|
+
authMode;
|
|
7
|
+
paidFetch;
|
|
5
8
|
constructor(config) {
|
|
6
9
|
this.config = config;
|
|
10
|
+
this.authMode = config.evmPrivateKey && !config.apiKey ? 'x402' : 'api-key';
|
|
11
|
+
if (!config.apiKey && config.evmPrivateKey) {
|
|
12
|
+
this.paidFetch = createPaidFetch(config.evmPrivateKey, config.x402EvmNetwork);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
resolveFetch(authenticated) {
|
|
16
|
+
if (authenticated && this.paidFetch) {
|
|
17
|
+
return this.paidFetch;
|
|
18
|
+
}
|
|
19
|
+
return fetch;
|
|
7
20
|
}
|
|
8
21
|
async request(options) {
|
|
9
22
|
const { method, path, body, query, timeoutMs } = options;
|
|
10
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
|
+
}
|
|
11
27
|
const url = new URL(path, `${this.config.baseUrl}/`);
|
|
12
28
|
if (query) {
|
|
13
29
|
for (const [key, value] of Object.entries(query)) {
|
|
@@ -19,7 +35,7 @@ export class ConnectClient {
|
|
|
19
35
|
const headers = {
|
|
20
36
|
accept: 'application/json',
|
|
21
37
|
};
|
|
22
|
-
if (authenticated) {
|
|
38
|
+
if (authenticated && this.config.apiKey) {
|
|
23
39
|
headers['x-api-key'] = this.config.apiKey;
|
|
24
40
|
}
|
|
25
41
|
if (body !== undefined) {
|
|
@@ -29,8 +45,9 @@ export class ConnectClient {
|
|
|
29
45
|
const timeout = timeoutMs
|
|
30
46
|
? setTimeout(() => controller.abort(), timeoutMs)
|
|
31
47
|
: undefined;
|
|
48
|
+
const httpFetch = this.resolveFetch(authenticated);
|
|
32
49
|
try {
|
|
33
|
-
const response = await
|
|
50
|
+
const response = await httpFetch(url, {
|
|
34
51
|
method,
|
|
35
52
|
headers,
|
|
36
53
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
@@ -39,9 +56,9 @@ export class ConnectClient {
|
|
|
39
56
|
const raw = await response.json().catch(() => ({}));
|
|
40
57
|
if (!response.ok) {
|
|
41
58
|
if (response.status === 504) {
|
|
42
|
-
return errorResult(mapHttpError(504, raw));
|
|
59
|
+
return errorResult(mapHttpError(504, raw, this.authMode));
|
|
43
60
|
}
|
|
44
|
-
return errorResult(mapHttpError(response.status, raw));
|
|
61
|
+
return errorResult(mapHttpError(response.status, raw, this.authMode));
|
|
45
62
|
}
|
|
46
63
|
return successResult({
|
|
47
64
|
httpStatus: response.status,
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export interface ConnectClientConfig {
|
|
2
2
|
baseUrl: string;
|
|
3
|
-
|
|
3
|
+
/** When set, sent on every authenticated Connect API request. */
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
/** When apiKey is unset, authenticated requests pay via x402 on 402. */
|
|
6
|
+
evmPrivateKey?: `0x${string}`;
|
|
7
|
+
/** EVM chain id for x402 (default 8453 = Base mainnet). */
|
|
8
|
+
x402EvmNetwork: number;
|
|
4
9
|
}
|
|
5
10
|
export interface HttpServerConfig {
|
|
6
11
|
host: string;
|
|
@@ -8,4 +13,11 @@ export interface HttpServerConfig {
|
|
|
8
13
|
baseUrl: string;
|
|
9
14
|
}
|
|
10
15
|
export declare function loadHttpServerConfig(env?: NodeJS.ProcessEnv): HttpServerConfig;
|
|
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
|
+
*/
|
|
11
21
|
export declare function loadConfig(env?: NodeJS.ProcessEnv): ConnectClientConfig;
|
|
22
|
+
/** Hosted HTTP MCP: api-key from request header only. */
|
|
23
|
+
export declare function createHostedClientConfig(apiKey: string, baseUrl: string): ConnectClientConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
+
import { normalizeEvmPrivateKey } from './x402-fetch.js';
|
|
1
2
|
const DEFAULT_BASE_URL = 'https://api.connect.quid.li';
|
|
2
3
|
const DEFAULT_HTTP_HOST = '0.0.0.0';
|
|
3
4
|
const DEFAULT_HTTP_PORT = 8080;
|
|
5
|
+
const DEFAULT_X402_EVM_NETWORK = 8453;
|
|
6
|
+
function parseX402EvmNetwork(env) {
|
|
7
|
+
const raw = env.CONNECT_X402_EVM_NETWORK?.trim() || env.X402_EVM_NETWORK?.trim() || String(DEFAULT_X402_EVM_NETWORK);
|
|
8
|
+
const network = Number(raw);
|
|
9
|
+
if (!Number.isInteger(network) || network < 1) {
|
|
10
|
+
throw new Error(`CONNECT_X402_EVM_NETWORK must be a positive integer (got "${raw}")`);
|
|
11
|
+
}
|
|
12
|
+
return network;
|
|
13
|
+
}
|
|
14
|
+
function loadBaseUrl(env) {
|
|
15
|
+
return (env.CONNECT_API_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
16
|
+
}
|
|
4
17
|
export function loadHttpServerConfig(env = process.env) {
|
|
5
18
|
const host = env.CONNECT_MCP_HTTP_HOST?.trim() || DEFAULT_HTTP_HOST;
|
|
6
19
|
const portRaw = env.CONNECT_MCP_HTTP_PORT?.trim() || String(DEFAULT_HTTP_PORT);
|
|
@@ -8,14 +21,29 @@ export function loadHttpServerConfig(env = process.env) {
|
|
|
8
21
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
9
22
|
throw new Error(`CONNECT_MCP_HTTP_PORT must be a valid port (got "${portRaw}")`);
|
|
10
23
|
}
|
|
11
|
-
|
|
12
|
-
return { host, port, baseUrl };
|
|
24
|
+
return { host, port, baseUrl: loadBaseUrl(env) };
|
|
13
25
|
}
|
|
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
|
+
*/
|
|
14
31
|
export function loadConfig(env = process.env) {
|
|
15
|
-
const apiKey = env.CONNECT_API_KEY?.trim();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
const apiKey = env.CONNECT_API_KEY?.trim() || undefined;
|
|
33
|
+
const evmPrivateKey = normalizeEvmPrivateKey(env.EVM_PRIVATE_KEY);
|
|
34
|
+
const x402EvmNetwork = parseX402EvmNetwork(env);
|
|
35
|
+
return {
|
|
36
|
+
baseUrl: loadBaseUrl(env),
|
|
37
|
+
apiKey,
|
|
38
|
+
evmPrivateKey: apiKey ? undefined : evmPrivateKey,
|
|
39
|
+
x402EvmNetwork,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Hosted HTTP MCP: api-key from request header only. */
|
|
43
|
+
export function createHostedClientConfig(apiKey, baseUrl) {
|
|
44
|
+
return {
|
|
45
|
+
baseUrl,
|
|
46
|
+
apiKey,
|
|
47
|
+
x402EvmNetwork: DEFAULT_X402_EVM_NETWORK,
|
|
48
|
+
};
|
|
21
49
|
}
|
package/dist/deeplink.js
CHANGED
|
@@ -7,7 +7,7 @@ export function buildCursorRemoteMcpConfig(apiKey = CONNECT_MCP_API_KEY_PLACEHOL
|
|
|
7
7
|
},
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
|
-
export function buildCursorMcpInstallDeeplink(serverName = 'connect', config = buildCursorRemoteMcpConfig()) {
|
|
10
|
+
export function buildCursorMcpInstallDeeplink(serverName = 'quidli-connect', config = buildCursorRemoteMcpConfig()) {
|
|
11
11
|
const encoded = Buffer.from(JSON.stringify(config), 'utf8').toString('base64');
|
|
12
12
|
return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(serverName)}&config=${encoded}`;
|
|
13
13
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export declare function parseNestErrorMessage(body: unknown): string;
|
|
2
|
-
export
|
|
2
|
+
export type ConnectAuthMode = 'api-key' | 'x402';
|
|
3
|
+
export declare function mapHttpError(status: number, body: unknown, authMode?: ConnectAuthMode): string;
|
package/dist/errors.js
CHANGED
|
@@ -10,15 +10,19 @@ export function parseNestErrorMessage(body) {
|
|
|
10
10
|
}
|
|
11
11
|
return 'Request failed';
|
|
12
12
|
}
|
|
13
|
-
export function mapHttpError(status, body) {
|
|
13
|
+
export function mapHttpError(status, body, authMode = 'api-key') {
|
|
14
14
|
const detail = parseNestErrorMessage(body);
|
|
15
15
|
switch (status) {
|
|
16
16
|
case 400:
|
|
17
17
|
return detail;
|
|
18
18
|
case 401:
|
|
19
|
-
return
|
|
19
|
+
return authMode === 'api-key'
|
|
20
|
+
? `Unauthorized: ${detail}. Verify CONNECT_API_KEY at https://connect.quid.li`
|
|
21
|
+
: `Unauthorized: ${detail}. Verify EVM_PRIVATE_KEY and wallet USDC balance for x402.`;
|
|
20
22
|
case 402:
|
|
21
|
-
return
|
|
23
|
+
return authMode === 'api-key'
|
|
24
|
+
? `Payment required (${detail}). Set CONNECT_API_KEY or use local stdio with EVM_PRIVATE_KEY for x402.`
|
|
25
|
+
: `Payment failed (${detail}). Ensure your wallet has USDC on the expected network and EVM_PRIVATE_KEY is correct.`;
|
|
22
26
|
case 404:
|
|
23
27
|
return detail;
|
|
24
28
|
case 504:
|
package/dist/index-http.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/mount.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
2
|
+
import { createHostedClientConfig } from './config.js';
|
|
2
3
|
import { createMcpServer } from './create-mcp-server.js';
|
|
3
4
|
import { validateRequestApiKey } from './http-auth.js';
|
|
4
5
|
export { resolveRequestHost } from './request-host.js';
|
|
@@ -49,7 +50,7 @@ export function mountConnectMcpHttp(router, options) {
|
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
51
52
|
try {
|
|
52
|
-
const server = createMcpServer(
|
|
53
|
+
const server = createMcpServer(createHostedClientConfig(auth.apiKey, baseUrl));
|
|
53
54
|
const transport = new StreamableHTTPServerTransport({
|
|
54
55
|
sessionIdGenerator: undefined, // stateless — no session tracking
|
|
55
56
|
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',
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { x402Client, wrapFetchWithPayment } from '@x402/fetch';
|
|
2
|
+
import { ExactEvmScheme } from '@x402/evm/exact/client';
|
|
3
|
+
import { toClientEvmSigner } from '@x402/evm';
|
|
4
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
5
|
+
import { createPublicClient, http } from 'viem';
|
|
6
|
+
import { base, baseSepolia } from 'viem/chains';
|
|
7
|
+
const EVM_CHAIN_BY_ID = {
|
|
8
|
+
8453: base,
|
|
9
|
+
84532: baseSepolia,
|
|
10
|
+
};
|
|
11
|
+
export function normalizeEvmPrivateKey(value) {
|
|
12
|
+
const trimmed = value?.trim();
|
|
13
|
+
if (!trimmed) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return (trimmed.startsWith('0x') ? trimmed : `0x${trimmed}`);
|
|
17
|
+
}
|
|
18
|
+
function getEvmPublicClient(chainId) {
|
|
19
|
+
const chain = EVM_CHAIN_BY_ID[chainId] ?? base;
|
|
20
|
+
return createPublicClient({ chain, transport: http() });
|
|
21
|
+
}
|
|
22
|
+
export function createPaidFetch(evmPrivateKey, chainId) {
|
|
23
|
+
const account = privateKeyToAccount(evmPrivateKey);
|
|
24
|
+
const evmSigner = toClientEvmSigner(account, getEvmPublicClient(chainId));
|
|
25
|
+
const client = new x402Client();
|
|
26
|
+
const scheme = new ExactEvmScheme(evmSigner);
|
|
27
|
+
client.register(`eip155:${chainId}`, scheme);
|
|
28
|
+
client.register('eip155:*', scheme);
|
|
29
|
+
return wrapFetchWithPayment(fetch, client);
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quidli/connect-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server for Quidli Connect product APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
"agent"
|
|
14
14
|
],
|
|
15
15
|
"homepage": "https://connect.quid.li",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Quidli/sns-backend.git",
|
|
19
|
+
"directory": "packages/connect-mcp"
|
|
20
|
+
},
|
|
16
21
|
"author": {
|
|
17
22
|
"name": "Quidli",
|
|
18
23
|
"url": "https://quid.xyz"
|
|
@@ -34,8 +39,8 @@
|
|
|
34
39
|
"./http": "./dist/http.js"
|
|
35
40
|
},
|
|
36
41
|
"scripts": {
|
|
37
|
-
"build": "tsc
|
|
38
|
-
"prepublishOnly": "npm run build",
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"prepublishOnly": "npm run build && chmod +x dist/index.js dist/index-http.js",
|
|
39
44
|
"start": "node dist/index.js",
|
|
40
45
|
"start:http": "node dist/index-http.js",
|
|
41
46
|
"test": "vitest run",
|
|
@@ -47,10 +52,14 @@
|
|
|
47
52
|
},
|
|
48
53
|
"dependencies": {
|
|
49
54
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
55
|
+
"@x402/evm": "~2.5.0",
|
|
56
|
+
"@x402/fetch": "~2.5.0",
|
|
50
57
|
"express": "^5.2.1",
|
|
58
|
+
"viem": "^2.50.4",
|
|
51
59
|
"zod": "^3.24.2"
|
|
52
60
|
},
|
|
53
61
|
"devDependencies": {
|
|
62
|
+
"@types/express": "^5.0.6",
|
|
54
63
|
"@types/node": "^22.13.10",
|
|
55
64
|
"typescript": "^5.8.2",
|
|
56
65
|
"vitest": "^3.0.9"
|