@quidli/connect-mcp 0.6.3 → 0.6.5

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dw/@quidli/connect-mcp.svg)](https://www.npmjs.com/package/@quidli/connect-mcp)
5
5
  [![license](https://img.shields.io/npm/l/@quidli/connect-mcp.svg)](./LICENSE)
6
6
 
7
- Open payments for multiplayer agents, powered by social reputation. Resolve a social handle to a wallet, check reputation, and send tokens — from Cursor, Claude Desktop, Claude Code, or any MCP-compatible client.
7
+ Open payments for multiplayer agents, powered by onchain reputation. Resolve a social handle to a wallet, check its onchain reputation, and send tokens — from Cursor, Claude Desktop, Claude Code, or any MCP-compatible client.
8
8
 
9
9
  Multiplayer means parties who don't already know each other — an agent paying a person, or another agent, with no prior relationship and no account to fall back on. Connect is how it verifies who it's dealing with before sending anything.
10
10
 
@@ -52,6 +52,8 @@ Then ask your agent:
52
52
 
53
53
  Resolves across GitHub, X, Telegram, Discord, LinkedIn, Farcaster, email and phone — and generates a wallet for people who have never used Quidli.
54
54
 
55
+ Then score what you find: `connect_scores_batch` returns onchain reputation for a batch of identities.
56
+
55
57
  Lookup, scores, and price run without a key under a shared anonymous quota. Add a [Connect API key](https://connect.quid.li) for higher limits, your own profile (`connect_me`), and Smart Send.
56
58
 
57
59
  ---
@@ -371,8 +373,12 @@ mistake.
371
373
  | `connect_scores_by_account` | Scores for one linked account |
372
374
  | `connect_scores_by_username` | Scores by Connect username |
373
375
  | `connect_me` | API key owner profile, scores, and linked accounts — **API key only** |
374
- | `connect_drop` | Smart Send (EVM batch or Solana SOL/SPL) — **API key only** |
376
+ | `connect_drop` | Smart Send (EVM batch or Solana SOL/SPL); accepts `trustFilter` to gate recipients by trust graph — **API key only** |
375
377
  | `connect_drop_balance` | Smart Send wallet balances on a chain — **API key only** |
378
+ | `connect_trust_create` | Create an on-chain trust attestation (EAS on Base) — **API key only** |
379
+ | `connect_trust_check` | Check whether identities sit in a trust graph at depth 1 |
380
+ | `connect_trust_graph` | List outgoing/incoming trust edges for an identity |
381
+ | `connect_trust_revoke` | Revoke active trust attestations by target — **API key only** |
376
382
 
377
383
 
378
384
  Ask your client to use these tools when you need Connect data or actions.
package/dist/mount.d.ts CHANGED
@@ -8,5 +8,8 @@ export interface ConnectMcpMountOptions {
8
8
  }
9
9
  export declare function resolveEmbeddedApiBaseUrl(port: number, env?: NodeJS.ProcessEnv): string;
10
10
  export declare function isConnectMcpHost(hostname: string, env?: NodeJS.ProcessEnv): boolean;
11
- export declare function normalizeAcceptHeader(req: Pick<Request, 'headers'>): void;
11
+ type NormalizableRequest = Pick<Request, 'headers'> & {
12
+ rawHeaders?: string[];
13
+ };
14
+ export declare function normalizeAcceptHeader(req: NormalizableRequest): void;
12
15
  export declare function mountConnectMcpHttp(router: IRouter, options: ConnectMcpMountOptions): void;
package/dist/mount.js CHANGED
@@ -51,12 +51,44 @@ export function isConnectMcpHost(hostname, env = process.env) {
51
51
  // wildcards). A client that genuinely accepts neither is left alone and still
52
52
  // gets its 406.
53
53
  const CANONICAL_ACCEPT = 'application/json, text/event-stream';
54
+ // The SDK's StreamableHTTPServerTransport (>= 1.26) is a wrapper that converts the
55
+ // Node request into a web-standard Request using @hono/node-server. That converter
56
+ // rebuilds the header set from `req.rawHeaders` and never reads `req.headers`, so
57
+ // writing only `req.headers.accept` is invisible to the transport and the 406 still
58
+ // fires. Write both: `headers` for anything else in the stack, `rawHeaders` for the
59
+ // SDK. Duplicate Accept entries are collapsed, because `new Headers()` would comma
60
+ // join them back into a value we did not intend.
61
+ function setAcceptHeader(req, value) {
62
+ req.headers.accept = value;
63
+ const raw = req.rawHeaders;
64
+ if (!Array.isArray(raw)) {
65
+ return;
66
+ }
67
+ const rebuilt = [];
68
+ let written = false;
69
+ for (let i = 0; i + 1 < raw.length; i += 2) {
70
+ const key = raw[i];
71
+ if (key.toLowerCase() === 'accept') {
72
+ if (!written) {
73
+ rebuilt.push(key, value);
74
+ written = true;
75
+ }
76
+ continue;
77
+ }
78
+ rebuilt.push(key, raw[i + 1]);
79
+ }
80
+ if (!written) {
81
+ rebuilt.push('Accept', value);
82
+ }
83
+ raw.length = 0;
84
+ raw.push(...rebuilt);
85
+ }
54
86
  export function normalizeAcceptHeader(req) {
55
87
  const raw = req.headers.accept;
56
88
  const value = Array.isArray(raw) ? raw.join(',') : (raw ?? '');
57
89
  // No Accept header means "anything" per RFC 9110.
58
90
  if (!value.trim()) {
59
- req.headers.accept = CANONICAL_ACCEPT;
91
+ setAcceptHeader(req, CANONICAL_ACCEPT);
60
92
  return;
61
93
  }
62
94
  const types = value
@@ -65,7 +97,7 @@ export function normalizeAcceptHeader(req) {
65
97
  .filter(Boolean);
66
98
  const permits = (type) => types.includes(type) || types.includes('*/*') || types.includes(`${type.split('/')[0]}/*`);
67
99
  if (permits('application/json') && permits('text/event-stream')) {
68
- req.headers.accept = CANONICAL_ACCEPT;
100
+ setAcceptHeader(req, CANONICAL_ACCEPT);
69
101
  }
70
102
  }
71
103
  export function mountConnectMcpHttp(router, options) {
@@ -34,39 +34,84 @@ const SPENDS_FUNDS = {
34
34
  openWorldHint: true,
35
35
  };
36
36
  export function registerTools(server, client) {
37
- server.tool('connect_get_price', 'Get public x402 list prices for lookup and scores (reference only; live paywall amounts are in 402 responses).', {}, READ_ONLY, async () => client.request({ method: 'GET', path: '/price', authenticated: false }));
38
- server.tool('connect_get_chains', 'List product chains with per-feature compatibility. lookup is true for EVM catalog chains and Solana; drop is true for Smart Send EVM chains and Solana (chainId 1399811149 for connect_drop / connect_drop_balance).', {}, READ_ONLY, async () => client.request({ method: 'GET', path: '/chains', authenticated: false }));
39
- server.tool('connect_lookup', 'Resolve social identities to EVM and Solana wallet addresses. Resolving a recipient who has no wallet provisions one for them, so this is read-only for the caller but not for the recipient. If status is processing, retry the same payload.', lookupInputSchema, READ_ONLY, async ({ recipients }) => client.request({
37
+ server.registerTool('connect_get_price', {
38
+ title: 'Get x402 Prices',
39
+ description: 'Get public x402 list prices for lookup and scores (reference only; live paywall amounts are in 402 responses).',
40
+ inputSchema: {},
41
+ annotations: { title: 'Get x402 Prices', ...READ_ONLY },
42
+ }, async () => client.request({ method: 'GET', path: '/price', authenticated: false }));
43
+ server.registerTool('connect_get_chains', {
44
+ title: 'List Supported Chains',
45
+ description: 'List product chains with per-feature compatibility. lookup is true for EVM catalog chains and Solana; drop is true for Smart Send EVM chains and Solana (chainId 1399811149 for connect_drop / connect_drop_balance).',
46
+ inputSchema: {},
47
+ annotations: { title: 'List Supported Chains', ...READ_ONLY },
48
+ }, async () => client.request({ method: 'GET', path: '/chains', authenticated: false }));
49
+ server.registerTool('connect_lookup', {
50
+ title: 'Resolve Handle to Wallet',
51
+ description: 'Resolve social identities to EVM and Solana wallet addresses. Resolving a recipient who has no wallet provisions one for them, so this is read-only for the caller but not for the recipient. If status is processing, retry the same payload.',
52
+ inputSchema: lookupInputSchema,
53
+ annotations: { title: 'Resolve Handle to Wallet', ...READ_ONLY },
54
+ }, async ({ recipients }) => client.request({
40
55
  method: 'POST',
41
56
  path: '/lookup',
42
57
  body: { recipients },
43
58
  }));
44
- server.tool('connect_lookup_exposed', 'List platforms a recipient has exposed on Connect, with enriched profile, scores, and wallet addresses. Recipient may be a social account, an exposed wallet (EVM/Solana/smart wallet), or a Connect username. May require x402 payment when the profile owner charges for lookups.', lookupExposedInputSchema, READ_ONLY, async ({ recipient }) => client.request({
59
+ server.registerTool('connect_lookup_exposed', {
60
+ title: 'List Exposed Accounts',
61
+ description: 'List platforms a recipient has exposed on Connect, with enriched profile, scores, and wallet addresses. Recipient may be a social account, an exposed wallet (EVM/Solana/smart wallet), or a Connect username. May require x402 payment when the profile owner charges for lookups.',
62
+ inputSchema: lookupExposedInputSchema,
63
+ annotations: { title: 'List Exposed Accounts', ...READ_ONLY },
64
+ }, async ({ recipient }) => client.request({
45
65
  method: 'POST',
46
66
  path: '/lookup/exposed',
47
67
  body: { recipient },
48
68
  }));
49
- server.tool('connect_scores_batch', 'Batch scores for linked accounts or Connect usernames. One result per request user. Optional filter sets passedFilter (quidli 0–100, neynar/lens 0–1, ethos 0–2800).', scoresBatchInputSchema, READ_ONLY, async ({ users, filter }) => client.request({
69
+ server.registerTool('connect_scores_batch', {
70
+ title: 'Score Identities (Batch)',
71
+ description: 'Batch scores for linked accounts or Connect usernames. One result per request user. Optional filter sets passedFilter (quidli 0–100, neynar/lens 0–1, ethos 0–2800).',
72
+ inputSchema: scoresBatchInputSchema,
73
+ annotations: { title: 'Score Identities (Batch)', ...READ_ONLY },
74
+ }, async ({ users, filter }) => client.request({
50
75
  method: 'POST',
51
76
  path: '/scores',
52
77
  body: { users, ...(filter ? { filter } : {}) },
53
78
  }));
54
- server.tool('connect_scores_by_account', 'Scores for a linked social account or wallet.', scoresByAccountInputSchema, READ_ONLY, async ({ platform, identifier }) => client.request({
79
+ server.registerTool('connect_scores_by_account', {
80
+ title: 'Score a Linked Account',
81
+ description: 'Scores for a linked social account or wallet.',
82
+ inputSchema: scoresByAccountInputSchema,
83
+ annotations: { title: 'Score a Linked Account', ...READ_ONLY },
84
+ }, async ({ platform, identifier }) => client.request({
55
85
  method: 'GET',
56
86
  path: `/scores/${encodeURIComponent(platform)}/${encodeURIComponent(identifier)}`,
57
87
  }));
58
- server.tool('connect_scores_by_username', 'Scores by Connect public username.', scoresByUsernameInputSchema, READ_ONLY, async ({ username }) => client.request({
88
+ server.registerTool('connect_scores_by_username', {
89
+ title: 'Score a Connect Username',
90
+ description: 'Scores by Connect public username.',
91
+ inputSchema: scoresByUsernameInputSchema,
92
+ annotations: { title: 'Score a Connect Username', ...READ_ONLY },
93
+ }, async ({ username }) => client.request({
59
94
  method: 'GET',
60
95
  path: `/scores/u/${encodeURIComponent(username.trim())}`,
61
96
  }));
62
- 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.', {}, READ_ONLY, async () => client.request({
97
+ server.registerTool('connect_me', {
98
+ title: 'Get My Connect Profile',
99
+ description: 'Get the Connect profile, scores, and all linked accounts for the API key owner. Use to identify which user the key belongs to.',
100
+ inputSchema: {},
101
+ annotations: { title: 'Get My Connect Profile', ...READ_ONLY },
102
+ }, async () => client.request({
63
103
  method: 'GET',
64
104
  path: '/account/me',
65
105
  }));
66
- server.tool('connect_drop', 'Execute a Smart Send from the API key owner Connect embedded wallet. EVM: batch native or ERC-20 (need native gas plus the token). Solana (chainId 1399811149): SOL or SPL from the Solana embedded wallet; packs up to 20 native or 10 SPL recipients per transaction. Social recipient types: email, phone, telegram, discord, farcaster, twitter, github (id or username). linkedin and slack are not on /drop — use connect_lookup first, then type wallet. After connect_lookup, EVM payouts use ethWalletAddress (never solWalletAddress); Solana payouts use solWalletAddress (never ethWalletAddress). Social types on /drop resolve server-side; for type wallet, pass the resolved payout address for the target chain. Omit tokenContract or set it to null for the native token; pass an ERC-20 contract or SPL mint otherwise — do not use the zero address. Amounts are smallest-unit integer strings (ETH 18 decimals, SOL 9, USDC usually 6). ' +
67
- 'Solana native (tokenContract null): no ATA. Sending to a recipient without an existing funded account requires amount ≥ 890880 lamports (rent-exempt minimum for a system account); that SOL stays with the recipient. Below that the tx fails. Sender also pays a ~5000-lamport fee. ' +
68
- 'Solana SPL: tokens sit in Associated Token Accounts (ATA), not on the wallet pubkey. Recipients need not already hold the token — the API prepends CreateIdempotent. The sender (not the recipient) pays ~2039280 lamports (~0.002039 SOL) rent per newly created dest ATA, plus tx fees, on top of the token amount (which can be as small as 1 unit). A 400 "Insufficient funds" on SPL is often missing SOL for ATA rent, not missing USDC. Token-2022 is not supported; USDC mint is EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v. ' +
69
- 'Always call connect_drop_balance first. Returns 201 when submitted or 202 when recipients still processing — retry with the same idempotencyKey.', dropInputSchema, SPENDS_FUNDS, async ({ ignoreFailedRecipients, ...body }) => client.request({
106
+ server.registerTool('connect_drop', {
107
+ title: 'Send Tokens (Smart Send)',
108
+ description: 'Execute a Smart Send from the API key owner Connect embedded wallet. EVM: batch native or ERC-20 (need native gas plus the token). Solana (chainId 1399811149): SOL or SPL from the Solana embedded wallet; packs up to 20 native or 10 SPL recipients per transaction. Social recipient types: email, phone, telegram, discord, farcaster, twitter, github (id or username). linkedin and slack are not on /drop — use connect_lookup first, then type wallet. After connect_lookup, EVM payouts use ethWalletAddress (never solWalletAddress); Solana payouts use solWalletAddress (never ethWalletAddress). Social types on /drop resolve server-side; for type wallet, pass the resolved payout address for the target chain. Omit tokenContract or set it to null for the native token; pass an ERC-20 contract or SPL mint otherwise — do not use the zero address. Amounts are smallest-unit integer strings (ETH 18 decimals, SOL 9, USDC usually 6). ' +
109
+ 'Solana native (tokenContract null): no ATA. Sending to a recipient without an existing funded account requires amount ≥ 890880 lamports (rent-exempt minimum for a system account); that SOL stays with the recipient. Below that the tx fails. Sender also pays a ~5000-lamport fee. ' +
110
+ 'Solana SPL: tokens sit in Associated Token Accounts (ATA), not on the wallet pubkey. Recipients need not already hold the token — the API prepends CreateIdempotent. The sender (not the recipient) pays ~2039280 lamports (~0.002039 SOL) rent per newly created dest ATA, plus tx fees, on top of the token amount (which can be as small as 1 unit). A 400 "Insufficient funds" on SPL is often missing SOL for ATA rent, not missing USDC. Token-2022 is not supported; USDC mint is EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v. ' +
111
+ 'Always call connect_drop_balance first. Returns 201 when submitted or 202 when recipients still processing — retry with the same idempotencyKey.',
112
+ inputSchema: dropInputSchema,
113
+ annotations: { title: 'Send Tokens (Smart Send)', ...SPENDS_FUNDS },
114
+ }, async ({ ignoreFailedRecipients, ...body }) => client.request({
70
115
  method: 'POST',
71
116
  path: '/drop',
72
117
  body,
@@ -75,32 +120,57 @@ export function registerTools(server, client) {
75
120
  ? { ignoreFailedRecipients: 'true' }
76
121
  : undefined,
77
122
  }));
78
- server.tool('connect_drop_balance', 'Get native and token balances for the API key owner Smart Send embedded wallet on a chain. Always call before connect_drop. Zero balances are omitted, so a missing token means balance 0. ' +
79
- 'EVM: confirm native gas plus the ERC-20 being sent. ' +
80
- 'Solana (chainId 1399811149): SOL in this response is spendable lamports on the wallet pubkey (rent locked in existing token accounts is not included). Native SOL drop to a recipient without an existing funded account: amount itself must be ≥ 890880 lamports and sender SOL must cover amount + ~5000 lamports fee. SPL drop: token balance ≥ total amount, and SOL ≥ tx fee + ~2039280 lamports (~0.002039 SOL) per recipient that may need a new Associated Token Account — even when sending USDC. Insufficient SOL for ATA rent fails before the token transfer.', dropBalanceInputSchema, READ_ONLY, async ({ chainId }) => client.request({
123
+ server.registerTool('connect_drop_balance', {
124
+ title: 'Check Smart Send Balance',
125
+ description: 'Get native and token balances for the API key owner Smart Send embedded wallet on a chain. Always call before connect_drop. Zero balances are omitted, so a missing token means balance 0. ' +
126
+ 'EVM: confirm native gas plus the ERC-20 being sent. ' +
127
+ 'Solana (chainId 1399811149): SOL in this response is spendable lamports on the wallet pubkey (rent locked in existing token accounts is not included). Native SOL drop to a recipient without an existing funded account: amount itself must be ≥ 890880 lamports and sender SOL must cover amount + ~5000 lamports fee. SPL drop: token balance ≥ total amount, and SOL ≥ tx fee + ~2039280 lamports (~0.002039 SOL) per recipient that may need a new Associated Token Account — even when sending USDC. Insufficient SOL for ATA rent fails before the token transfer.',
128
+ inputSchema: dropBalanceInputSchema,
129
+ annotations: { title: 'Check Smart Send Balance', ...READ_ONLY },
130
+ }, async ({ chainId }) => client.request({
81
131
  method: 'GET',
82
132
  path: '/drop/balance',
83
133
  requireApiKey: true,
84
134
  query: { chainId: String(chainId) },
85
135
  }));
86
- server.tool('connect_trust_create', 'Create a unidirectional trust attestation on Base (EAS) from the API key owner embedded wallet to a wallet or social identity. Identical active attestations are returned without a new transaction. Requires CONNECT_API_KEY, Smart Send attestation signer enrolled, and ETH on Base for gas.', trustCreateInputSchema, SPENDS_FUNDS, async (body) => client.request({
136
+ server.registerTool('connect_trust_create', {
137
+ title: 'Create Trust Attestation',
138
+ description: 'Create a unidirectional trust attestation on Base (EAS) from the API key owner embedded wallet to a wallet or social identity. Identical active attestations are returned without a new transaction. Requires CONNECT_API_KEY, Smart Send attestation signer enrolled, and ETH on Base for gas.',
139
+ inputSchema: trustCreateInputSchema,
140
+ annotations: { title: 'Create Trust Attestation', ...SPENDS_FUNDS },
141
+ }, async (body) => client.request({
87
142
  method: 'POST',
88
143
  path: '/trust',
89
144
  body,
90
145
  requireApiKey: true,
91
146
  }));
92
- server.tool('connect_trust_revoke', 'Revoke active trust attestations from the API key owner to a target. Optional context limits which attestations are revoked. Requires CONNECT_API_KEY and ETH on Base.', trustRevokeInputSchema, SPENDS_FUNDS, async (body) => client.request({
147
+ server.registerTool('connect_trust_revoke', {
148
+ title: 'Revoke Trust Attestation',
149
+ description: 'Revoke active trust attestations from the API key owner to a target. Optional context limits which attestations are revoked. Requires CONNECT_API_KEY and ETH on Base.',
150
+ inputSchema: trustRevokeInputSchema,
151
+ annotations: { title: 'Revoke Trust Attestation', ...SPENDS_FUNDS },
152
+ }, async (body) => client.request({
93
153
  method: 'POST',
94
154
  path: '/trust/revoke',
95
155
  body,
96
156
  requireApiKey: true,
97
157
  }));
98
- server.tool('connect_trust_check', 'Check whether identities sit in a trust graph at depth 1. Omit context to match any context.', trustCheckInputSchema, READ_ONLY, async (body) => client.request({
158
+ server.registerTool('connect_trust_check', {
159
+ title: 'Check Trust',
160
+ description: 'Check whether identities sit in a trust graph at depth 1. Omit context to match any context.',
161
+ inputSchema: trustCheckInputSchema,
162
+ annotations: { title: 'Check Trust', ...READ_ONLY },
163
+ }, async (body) => client.request({
99
164
  method: 'POST',
100
165
  path: '/trust/check',
101
166
  body,
102
167
  }));
103
- server.tool('connect_trust_graph', 'List outgoing or incoming trust edges for an identity at depth 1.', trustGraphInputSchema, READ_ONLY, async ({ platform, identifier, context, direction }) => client.request({
168
+ server.registerTool('connect_trust_graph', {
169
+ title: 'List Trust Edges',
170
+ description: 'List outgoing or incoming trust edges for an identity at depth 1.',
171
+ inputSchema: trustGraphInputSchema,
172
+ annotations: { title: 'List Trust Edges', ...READ_ONLY },
173
+ }, async ({ platform, identifier, context, direction }) => client.request({
104
174
  method: 'GET',
105
175
  path: `/trust/graph/${encodeURIComponent(platform)}/${encodeURIComponent(identifier)}`,
106
176
  query: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quidli/connect-mcp",
3
- "version": "0.6.3",
4
- "description": "MCP server for agent identity and reputation: resolve social handles to wallet addresses, score onchain reputation, and send USDC from any MCP client.",
3
+ "version": "0.6.5",
4
+ "description": "Onchain reputation and identity for AI agents: score onchain reputation across social accounts, resolve handles to EVM and Solana wallet addresses, and send USDC from any MCP client.",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "mcp-server",