@quidli/connect-mcp 0.1.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 ADDED
@@ -0,0 +1,143 @@
1
+ # @quidli/connect-mcp
2
+
3
+ MCP server exposing [Quidli Connect](https://connect.quid.li) product APIs as tools for Cursor, Claude Desktop, and other MCP hosts.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 20+
8
+ - A Connect API key ([Enable API access](https://connect.quid.li))
9
+
10
+ ## Cursor configuration
11
+
12
+ ### Remote URL (recommended for production)
13
+
14
+ Same pattern as GitHub MCP — Cursor connects to a hosted endpoint and sends your API key in headers:
15
+
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "connect": {
20
+ "url": "https://mcp.connect.quid.li",
21
+ "headers": {
22
+ "x-api-key": "set-your-api-key-here"
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ After one-click install, open **Cursor Settings → MCP → connect** and replace `set-your-api-key-here` with your real API key from [connect.quid.li](https://connect.quid.li) (Enable API access). Until you do, the server returns `401` with setup instructions.
30
+
31
+ **Add to Cursor** deeplink (placeholder key pre-filled):
32
+
33
+ ```
34
+ cursor://anysphere.cursor-deeplink/mcp/install?name=connect&config=eyJ1cmwiOiJodHRwczovL21jcC5jb25uZWN0LnF1aWQubGkIiwiaGVhZGVycyI6eyJ4LWFwaS1rZXkiOiJzZXQteW91ci1hcGkta2V5LWhlcmUifX0=
35
+ ```
36
+
37
+ Or generate programmatically:
38
+
39
+ ```typescript
40
+ import { buildCursorMcpInstallDeeplink } from '@quidli/connect-mcp/deeplink';
41
+ // buildCursorMcpInstallDeeplink() in published package once exported — see src/deeplink.ts
42
+ ```
43
+
44
+ ### Local stdio (npm / development)
45
+
46
+ Add to `.cursor/mcp.json` (or global MCP settings):
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "connect": {
52
+ "command": "npx",
53
+ "args": ["-y", "@quidli/connect-mcp"],
54
+ "env": {
55
+ "CONNECT_API_KEY": "<your-api-key>",
56
+ "CONNECT_API_BASE_URL": "https://api.connect.quid.li"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ For local development from this monorepo:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "connect-local": {
69
+ "command": "node",
70
+ "args": ["/absolute/path/to/connect-backend/packages/connect-mcp/dist/index.js"],
71
+ "env": {
72
+ "CONNECT_API_KEY": "<your-api-key>",
73
+ "CONNECT_API_BASE_URL": "https://api.staging.connect.quid.li"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ `CONNECT_API_KEY` is **required** for stdio mode — the process exits at startup if missing. Remote HTTP mode reads `x-api-key` from each request instead.
81
+
82
+ ## HTTP server (hosted deployment)
83
+
84
+ ### Embedded in connect-backend (production)
85
+
86
+ MCP runs inside the **same Cloud Run service** as `api.connect.quid.li`. Map `mcp.connect.quid.li` as an additional custom domain on that service. Requests with MCP `Host` headers are routed to the MCP handler; API traffic is unchanged.
87
+
88
+ See `docs/ai/deployment/feature-connect-api-mcp.md` in connect-backend for DNS, env vars, and smoke tests.
89
+
90
+ ### Standalone binary (local / debugging)
91
+
92
+ ```bash
93
+ cd packages/connect-mcp
94
+ npm run build
95
+ CONNECT_API_BASE_URL=https://api.connect.quid.li npm run start:http
96
+ ```
97
+
98
+ Environment:
99
+
100
+ | Variable | Default | Description |
101
+ |----------|---------|-------------|
102
+ | `CONNECT_MCP_HTTP_HOST` | `0.0.0.0` | Bind address |
103
+ | `CONNECT_MCP_HTTP_PORT` | `8080` | Listen port |
104
+ | `CONNECT_API_BASE_URL` | `https://api.connect.quid.li` | Upstream Connect API |
105
+
106
+ Health check: `GET /health`
107
+
108
+ ## Tools (7)
109
+
110
+ | Tool | HTTP | Auth |
111
+ |------|------|------|
112
+ | `connect_get_price` | `GET /price` | none |
113
+ | `connect_lookup` | `POST /lookup` | api-key |
114
+ | `connect_scores_batch` | `POST /scores` | api-key |
115
+ | `connect_scores_by_account` | `GET /scores/:platform/:identifier` | api-key |
116
+ | `connect_scores_by_username` | `GET /scores/u/:username` | api-key |
117
+ | `connect_drop` | `POST /drop` | api-key |
118
+ | `connect_agent_prompt` | `POST /agent` | api-key |
119
+
120
+ Not exposed: `POST /lookup/exposed`, agent feedback, member/admin routes.
121
+
122
+ ## Auth notes
123
+
124
+ - **MCP uses api-key only.** x402 pay-per-call and Privy Bearer are HTTP-only (use [API docs](https://connect.quid.li/docs) or skills directly).
125
+ - `connect_get_price` is public on the HTTP API and does not send `x-api-key`.
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ cd packages/connect-mcp
131
+ npm install
132
+ npm test
133
+ npm run build
134
+ CONNECT_API_KEY=<key> npm start
135
+ ```
136
+
137
+ ## Publish
138
+
139
+ ```bash
140
+ npm publish --access public
141
+ ```
142
+
143
+ (Requires `@quidli` npm org access.)
@@ -0,0 +1,16 @@
1
+ import type { ConnectClientConfig } from './config.js';
2
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3
+ export type HttpMethod = 'GET' | 'POST';
4
+ export interface ConnectRequestOptions {
5
+ method: HttpMethod;
6
+ path: string;
7
+ authenticated?: boolean;
8
+ body?: unknown;
9
+ query?: Record<string, string | undefined>;
10
+ timeoutMs?: number;
11
+ }
12
+ export declare class ConnectClient {
13
+ private readonly config;
14
+ constructor(config: ConnectClientConfig);
15
+ request(options: ConnectRequestOptions): Promise<CallToolResult>;
16
+ }
package/dist/client.js ADDED
@@ -0,0 +1,64 @@
1
+ import { mapHttpError } from './errors.js';
2
+ import { errorResult, successResult } from './result.js';
3
+ export class ConnectClient {
4
+ config;
5
+ constructor(config) {
6
+ this.config = config;
7
+ }
8
+ async request(options) {
9
+ const { method, path, body, query, timeoutMs } = options;
10
+ const authenticated = options.authenticated ?? true;
11
+ const url = new URL(path, `${this.config.baseUrl}/`);
12
+ if (query) {
13
+ for (const [key, value] of Object.entries(query)) {
14
+ if (value !== undefined) {
15
+ url.searchParams.set(key, value);
16
+ }
17
+ }
18
+ }
19
+ const headers = {
20
+ accept: 'application/json',
21
+ };
22
+ if (authenticated) {
23
+ headers['x-api-key'] = this.config.apiKey;
24
+ }
25
+ if (body !== undefined) {
26
+ headers['content-type'] = 'application/json';
27
+ }
28
+ const controller = new AbortController();
29
+ const timeout = timeoutMs
30
+ ? setTimeout(() => controller.abort(), timeoutMs)
31
+ : undefined;
32
+ try {
33
+ const response = await fetch(url, {
34
+ method,
35
+ headers,
36
+ body: body !== undefined ? JSON.stringify(body) : undefined,
37
+ signal: controller.signal,
38
+ });
39
+ const raw = await response.json().catch(() => ({}));
40
+ if (!response.ok) {
41
+ if (response.status === 504) {
42
+ return errorResult(mapHttpError(504, raw));
43
+ }
44
+ return errorResult(mapHttpError(response.status, raw));
45
+ }
46
+ return successResult({
47
+ httpStatus: response.status,
48
+ ...(typeof raw === 'object' && raw !== null ? raw : { data: raw }),
49
+ });
50
+ }
51
+ catch (error) {
52
+ if (error instanceof Error && error.name === 'AbortError') {
53
+ return errorResult('Request timed out');
54
+ }
55
+ const message = error instanceof Error ? error.message : 'Network error';
56
+ return errorResult(message);
57
+ }
58
+ finally {
59
+ if (timeout) {
60
+ clearTimeout(timeout);
61
+ }
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,11 @@
1
+ export interface ConnectClientConfig {
2
+ baseUrl: string;
3
+ apiKey: string;
4
+ }
5
+ export interface HttpServerConfig {
6
+ host: string;
7
+ port: number;
8
+ baseUrl: string;
9
+ }
10
+ export declare function loadHttpServerConfig(env?: NodeJS.ProcessEnv): HttpServerConfig;
11
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): ConnectClientConfig;
package/dist/config.js ADDED
@@ -0,0 +1,21 @@
1
+ const DEFAULT_BASE_URL = 'https://api.connect.quid.li';
2
+ const DEFAULT_HTTP_HOST = '0.0.0.0';
3
+ const DEFAULT_HTTP_PORT = 8080;
4
+ export function loadHttpServerConfig(env = process.env) {
5
+ const host = env.CONNECT_MCP_HTTP_HOST?.trim() || DEFAULT_HTTP_HOST;
6
+ const portRaw = env.CONNECT_MCP_HTTP_PORT?.trim() || String(DEFAULT_HTTP_PORT);
7
+ const port = Number(portRaw);
8
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
9
+ throw new Error(`CONNECT_MCP_HTTP_PORT must be a valid port (got "${portRaw}")`);
10
+ }
11
+ const baseUrl = (env.CONNECT_API_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/$/, '');
12
+ return { host, port, baseUrl };
13
+ }
14
+ export function loadConfig(env = process.env) {
15
+ const apiKey = env.CONNECT_API_KEY?.trim();
16
+ if (!apiKey) {
17
+ throw new Error('CONNECT_API_KEY is required. Get a key at https://connect.quid.li (Enable API access).');
18
+ }
19
+ const baseUrl = (env.CONNECT_API_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/$/, '');
20
+ return { apiKey, baseUrl };
21
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ConnectClientConfig } from './config.js';
3
+ export declare function createMcpServer(config: ConnectClientConfig): McpServer;
@@ -0,0 +1,12 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { ConnectClient } from './client.js';
3
+ import { CONNECT_MCP_SERVER_INFO } from './metadata.js';
4
+ import { registerTools } from './tools/register.js';
5
+ export function createMcpServer(config) {
6
+ const server = new McpServer({
7
+ ...CONNECT_MCP_SERVER_INFO,
8
+ });
9
+ const client = new ConnectClient(config);
10
+ registerTools(server, client);
11
+ return server;
12
+ }
@@ -0,0 +1,8 @@
1
+ export interface CursorMcpRemoteConfig {
2
+ url: string;
3
+ headers: {
4
+ 'x-api-key': string;
5
+ };
6
+ }
7
+ export declare function buildCursorRemoteMcpConfig(apiKey?: string): CursorMcpRemoteConfig;
8
+ export declare function buildCursorMcpInstallDeeplink(serverName?: string, config?: CursorMcpRemoteConfig): string;
@@ -0,0 +1,13 @@
1
+ import { CONNECT_MCP_API_KEY_PLACEHOLDER, CONNECT_MCP_REMOTE_URL } from './metadata.js';
2
+ export function buildCursorRemoteMcpConfig(apiKey = CONNECT_MCP_API_KEY_PLACEHOLDER) {
3
+ return {
4
+ url: CONNECT_MCP_REMOTE_URL,
5
+ headers: {
6
+ 'x-api-key': apiKey,
7
+ },
8
+ };
9
+ }
10
+ export function buildCursorMcpInstallDeeplink(serverName = 'connect', config = buildCursorRemoteMcpConfig()) {
11
+ const encoded = Buffer.from(JSON.stringify(config), 'utf8').toString('base64');
12
+ return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(serverName)}&config=${encoded}`;
13
+ }
@@ -0,0 +1,2 @@
1
+ export declare function parseNestErrorMessage(body: unknown): string;
2
+ export declare function mapHttpError(status: number, body: unknown): string;
package/dist/errors.js ADDED
@@ -0,0 +1,31 @@
1
+ export function parseNestErrorMessage(body) {
2
+ if (body && typeof body === 'object' && 'message' in body) {
3
+ const message = body.message;
4
+ if (Array.isArray(message)) {
5
+ return message.map(String).join(' ');
6
+ }
7
+ if (typeof message === 'string') {
8
+ return message;
9
+ }
10
+ }
11
+ return 'Request failed';
12
+ }
13
+ export function mapHttpError(status, body) {
14
+ const detail = parseNestErrorMessage(body);
15
+ switch (status) {
16
+ case 400:
17
+ return detail;
18
+ case 401:
19
+ return `Unauthorized: ${detail}. Verify CONNECT_API_KEY at https://connect.quid.li`;
20
+ case 402:
21
+ return `Payment required (${detail}). MCP requires a valid Connect API key — x402 is not supported in MCP.`;
22
+ case 404:
23
+ return detail;
24
+ case 504:
25
+ return detail === 'Request failed'
26
+ ? 'Upstream agent did not respond within 60 seconds'
27
+ : detail;
28
+ default:
29
+ return status >= 500 ? `Connect API error (${status}): ${detail}` : detail;
30
+ }
31
+ }
@@ -0,0 +1,13 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ export interface RequestApiKeyResult {
3
+ ok: true;
4
+ apiKey: string;
5
+ }
6
+ export interface RequestApiKeyError {
7
+ ok: false;
8
+ message: string;
9
+ }
10
+ export type RequestApiKeyValidation = RequestApiKeyResult | RequestApiKeyError;
11
+ export declare function extractRequestApiKey(req: IncomingMessage): string | undefined;
12
+ export declare function isPlaceholderApiKey(apiKey: string): boolean;
13
+ export declare function validateRequestApiKey(req: IncomingMessage): RequestApiKeyValidation;
@@ -0,0 +1,32 @@
1
+ import { CONNECT_MCP_API_KEY_PLACEHOLDER } from './metadata.js';
2
+ const PLACEHOLDER_API_KEYS = new Set([
3
+ 'your-connect-api-key',
4
+ '<your-api-key>',
5
+ 'set-your-api-key-here',
6
+ ...(CONNECT_MCP_API_KEY_PLACEHOLDER ? [CONNECT_MCP_API_KEY_PLACEHOLDER] : []),
7
+ ]);
8
+ export function extractRequestApiKey(req) {
9
+ const raw = req.headers['x-api-key'];
10
+ const value = Array.isArray(raw) ? raw[0] : raw;
11
+ const trimmed = value?.trim();
12
+ return trimmed || undefined;
13
+ }
14
+ export function isPlaceholderApiKey(apiKey) {
15
+ return PLACEHOLDER_API_KEYS.has(apiKey);
16
+ }
17
+ export function validateRequestApiKey(req) {
18
+ const apiKey = extractRequestApiKey(req);
19
+ if (!apiKey) {
20
+ return {
21
+ ok: false,
22
+ message: 'Missing x-api-key header. Get a key at https://connect.quid.li (Enable API access) and set it in your MCP headers.',
23
+ };
24
+ }
25
+ if (isPlaceholderApiKey(apiKey)) {
26
+ return {
27
+ ok: false,
28
+ message: 'Invalid API key. Set your Connect API key in Cursor MCP settings (headers.x-api-key). Get one at https://connect.quid.li',
29
+ };
30
+ }
31
+ return { ok: true, apiKey };
32
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { isConnectMcpHost, mountConnectMcpHttp, resolveEmbeddedApiBaseUrl } from './mount.js';
2
+ export declare function main(): Promise<void>;
package/dist/http.js ADDED
@@ -0,0 +1,19 @@
1
+ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
2
+ import { loadHttpServerConfig } from './config.js';
3
+ import { mountConnectMcpHttp } from './mount.js';
4
+ export { isConnectMcpHost, mountConnectMcpHttp, resolveEmbeddedApiBaseUrl } from './mount.js';
5
+ export async function main() {
6
+ const { host, port, baseUrl } = loadHttpServerConfig();
7
+ const app = createMcpExpressApp({ host, allowedHosts: ['mcp.connect.quid.li', 'localhost'] });
8
+ mountConnectMcpHttp(app, { baseUrl });
9
+ await new Promise((resolve, reject) => {
10
+ app.listen(port, host, (error) => {
11
+ if (error) {
12
+ reject(error);
13
+ return;
14
+ }
15
+ console.log(`Quidli Connect MCP HTTP server listening on http://${host}:${port}/`);
16
+ resolve();
17
+ });
18
+ });
19
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './http.js';
3
+ main().catch((error) => {
4
+ const message = error instanceof Error ? error.message : String(error);
5
+ console.error(message);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './server.js';
3
+ main().catch((error) => {
4
+ const message = error instanceof Error ? error.message : String(error);
5
+ console.error(message);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,13 @@
1
+ export declare const CONNECT_MCP_API_KEY_PLACEHOLDER = "";
2
+ export declare const CONNECT_MCP_REMOTE_URL = "https://mcp.connect.quid.li";
3
+ export declare const CONNECT_MCP_SERVER_INFO: {
4
+ name: string;
5
+ title: string;
6
+ version: string;
7
+ websiteUrl: string;
8
+ icons: {
9
+ src: string;
10
+ mimeType: string;
11
+ sizes: string[];
12
+ }[];
13
+ };
@@ -0,0 +1,20 @@
1
+ export const CONNECT_MCP_API_KEY_PLACEHOLDER = '';
2
+ export const CONNECT_MCP_REMOTE_URL = 'https://mcp.connect.quid.li';
3
+ export const CONNECT_MCP_SERVER_INFO = {
4
+ name: 'quidli-connect',
5
+ title: 'Quidli Connect',
6
+ version: '0.1.0',
7
+ websiteUrl: 'https://connect.quid.li',
8
+ icons: [
9
+ {
10
+ src: 'https://connect.quid.li/theme/dark/squid.png',
11
+ mimeType: 'image/png',
12
+ sizes: ['48x48'],
13
+ },
14
+ {
15
+ src: 'https://assets.quid.li/icons/quidli/quidli_square.png',
16
+ mimeType: 'image/png',
17
+ sizes: ['64x64'],
18
+ },
19
+ ],
20
+ };
@@ -0,0 +1,8 @@
1
+ import type { IRouter } from 'express';
2
+ export { resolveRequestHost } from './request-host.js';
3
+ export interface ConnectMcpMountOptions {
4
+ baseUrl: string;
5
+ }
6
+ export declare function resolveEmbeddedApiBaseUrl(port: number, env?: NodeJS.ProcessEnv): string;
7
+ export declare function isConnectMcpHost(hostname: string, env?: NodeJS.ProcessEnv): boolean;
8
+ export declare function mountConnectMcpHttp(router: IRouter, options: ConnectMcpMountOptions): void;
package/dist/mount.js ADDED
@@ -0,0 +1,70 @@
1
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
2
+ import { createMcpServer } from './create-mcp-server.js';
3
+ import { validateRequestApiKey } from './http-auth.js';
4
+ export { resolveRequestHost } from './request-host.js';
5
+ function sendJsonRpcError(res, status, message) {
6
+ if (res.headersSent) {
7
+ return;
8
+ }
9
+ res.status(status).json({
10
+ jsonrpc: '2.0',
11
+ error: {
12
+ code: -32001,
13
+ message,
14
+ },
15
+ id: null,
16
+ });
17
+ }
18
+ export function resolveEmbeddedApiBaseUrl(port, env = process.env) {
19
+ const fromEnv = env.CONNECT_MCP_API_BASE_URL?.trim() || env.CONNECT_API_BASE_URL?.trim();
20
+ if (fromEnv) {
21
+ return fromEnv.replace(/\/$/, '');
22
+ }
23
+ return `http://127.0.0.1:${port}`;
24
+ }
25
+ export function isConnectMcpHost(hostname, env = process.env) {
26
+ const configured = env.CONNECT_MCP_HOSTS?.split(',')
27
+ .map((host) => host.trim().toLowerCase())
28
+ .filter(Boolean);
29
+ const normalized = hostname.toLowerCase();
30
+ if (configured?.length) {
31
+ return configured.includes(normalized);
32
+ }
33
+ return (normalized === 'mcp.connect.quid.li' ||
34
+ normalized === 'mcp.staging.connect.quid.li' ||
35
+ normalized.endsWith('.mcp.connect.quid.li'));
36
+ }
37
+ export function mountConnectMcpHttp(router, options) {
38
+ const { baseUrl } = options;
39
+ router.get('/health', (_req, res) => {
40
+ res.json({ ok: true, service: 'quidli-connect-mcp' });
41
+ });
42
+ // Stateless mode: each request creates a fresh server+transport.
43
+ // No in-memory session map, so Cloud Run restarts / multiple instances
44
+ // never cause "Missing or invalid MCP session" errors.
45
+ router.all('/', async (req, res) => {
46
+ const auth = validateRequestApiKey(req);
47
+ if (!auth.ok) {
48
+ sendJsonRpcError(res, 401, auth.message);
49
+ return;
50
+ }
51
+ try {
52
+ const server = createMcpServer({ apiKey: auth.apiKey, baseUrl });
53
+ const transport = new StreamableHTTPServerTransport({
54
+ sessionIdGenerator: undefined, // stateless — no session tracking
55
+ enableJsonResponse: true,
56
+ });
57
+ await server.connect(transport);
58
+ await transport.handleRequest(req, res, req.body);
59
+ res.on('close', () => {
60
+ transport.close().catch(() => undefined);
61
+ server.close().catch(() => undefined);
62
+ });
63
+ }
64
+ catch (error) {
65
+ const message = error instanceof Error ? error.message : 'Internal server error';
66
+ console.error('MCP HTTP error:', message);
67
+ sendJsonRpcError(res, 500, message);
68
+ }
69
+ });
70
+ }
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ export declare function resolveRequestHost(req: IncomingMessage): string;
@@ -0,0 +1,13 @@
1
+ export function resolveRequestHost(req) {
2
+ const forwarded = req.headers['x-forwarded-host'];
3
+ const forwardedHost = Array.isArray(forwarded) ? forwarded[0] : forwarded;
4
+ if (forwardedHost) {
5
+ return forwardedHost.split(',')[0]?.trim().split(':')[0]?.toLowerCase() ?? '';
6
+ }
7
+ const host = req.headers.host;
8
+ if (host) {
9
+ return host.split(':')[0]?.toLowerCase() ?? '';
10
+ }
11
+ const hostname = 'hostname' in req && typeof req.hostname === 'string' ? req.hostname : '';
12
+ return hostname.toLowerCase();
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ export declare function successResult(data: unknown): CallToolResult;
3
+ export declare function errorResult(message: string): CallToolResult;
package/dist/result.js ADDED
@@ -0,0 +1,11 @@
1
+ export function successResult(data) {
2
+ return {
3
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
4
+ };
5
+ }
6
+ export function errorResult(message) {
7
+ return {
8
+ isError: true,
9
+ content: [{ type: 'text', text: message }],
10
+ };
11
+ }
@@ -0,0 +1,86 @@
1
+ import { z } from 'zod';
2
+ /** Unified recipient — API validates fully; MCP forwards JSON. */
3
+ export declare const linkedAccountSchema: z.ZodObject<{
4
+ type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
5
+ id: z.ZodOptional<z.ZodString>;
6
+ username: z.ZodOptional<z.ZodString>;
7
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
8
+ type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
9
+ id: z.ZodOptional<z.ZodString>;
10
+ username: z.ZodOptional<z.ZodString>;
11
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
12
+ type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
13
+ id: z.ZodOptional<z.ZodString>;
14
+ username: z.ZodOptional<z.ZodString>;
15
+ }, z.ZodTypeAny, "passthrough">>;
16
+ export declare const lookupInputSchema: {
17
+ recipients: z.ZodArray<z.ZodObject<{
18
+ type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
19
+ id: z.ZodOptional<z.ZodString>;
20
+ username: z.ZodOptional<z.ZodString>;
21
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
22
+ type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
23
+ id: z.ZodOptional<z.ZodString>;
24
+ username: z.ZodOptional<z.ZodString>;
25
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
26
+ type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
27
+ id: z.ZodOptional<z.ZodString>;
28
+ username: z.ZodOptional<z.ZodString>;
29
+ }, z.ZodTypeAny, "passthrough">>, "many">;
30
+ };
31
+ export declare const scoresBatchInputSchema: {
32
+ users: z.ZodArray<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">>, "many">;
45
+ filter: z.ZodOptional<z.ZodObject<{
46
+ type: z.ZodEnum<["quidli_score", "lens_score", "neynar_score", "ethos_twitter_reputation", "ethos_wallet_reputation"]>;
47
+ minScore: z.ZodNumber;
48
+ }, "strip", z.ZodTypeAny, {
49
+ type: "quidli_score" | "lens_score" | "neynar_score" | "ethos_twitter_reputation" | "ethos_wallet_reputation";
50
+ minScore: number;
51
+ }, {
52
+ type: "quidli_score" | "lens_score" | "neynar_score" | "ethos_twitter_reputation" | "ethos_wallet_reputation";
53
+ minScore: number;
54
+ }>>;
55
+ };
56
+ export declare const scoresByAccountInputSchema: {
57
+ platform: z.ZodEnum<["wallet", "email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack"]>;
58
+ identifier: z.ZodString;
59
+ };
60
+ export declare const scoresByUsernameInputSchema: {
61
+ username: z.ZodString;
62
+ };
63
+ export declare const dropInputSchema: {
64
+ idempotencyKey: z.ZodString;
65
+ chainId: z.ZodNumber;
66
+ tokenContract: z.ZodOptional<z.ZodNullable<z.ZodString>>;
67
+ recipients: z.ZodArray<z.ZodObject<{
68
+ type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
69
+ id: z.ZodOptional<z.ZodString>;
70
+ username: z.ZodOptional<z.ZodString>;
71
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
72
+ type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
73
+ id: z.ZodOptional<z.ZodString>;
74
+ username: z.ZodOptional<z.ZodString>;
75
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
76
+ type: z.ZodEnum<["email", "phone", "telegram", "discord", "farcaster", "twitter", "github", "linkedin", "slack", "wallet", "username"]>;
77
+ id: z.ZodOptional<z.ZodString>;
78
+ username: z.ZodOptional<z.ZodString>;
79
+ }, z.ZodTypeAny, "passthrough">>, "many">;
80
+ amountInWeiPerRecipient: z.ZodNullable<z.ZodOptional<z.ZodString>>;
81
+ ignoreFailedRecipients: z.ZodOptional<z.ZodBoolean>;
82
+ };
83
+ export declare const agentPromptInputSchema: {
84
+ prompt: z.ZodString;
85
+ sessionId: z.ZodOptional<z.ZodString>;
86
+ };
@@ -0,0 +1,73 @@
1
+ import { z } from 'zod';
2
+ const recipientTypeSchema = z.enum([
3
+ 'email',
4
+ 'phone',
5
+ 'telegram',
6
+ 'discord',
7
+ 'farcaster',
8
+ 'twitter',
9
+ 'github',
10
+ 'linkedin',
11
+ 'slack',
12
+ 'wallet',
13
+ 'username',
14
+ ]);
15
+ /** Unified recipient — API validates fully; MCP forwards JSON. */
16
+ export const linkedAccountSchema = z
17
+ .object({
18
+ type: recipientTypeSchema,
19
+ id: z.string().optional(),
20
+ username: z.string().optional(),
21
+ })
22
+ .passthrough();
23
+ export const lookupInputSchema = {
24
+ recipients: z
25
+ .array(linkedAccountSchema)
26
+ .min(1)
27
+ .describe('Recipients to resolve to EVM/Solana wallets'),
28
+ };
29
+ export const scoresBatchInputSchema = {
30
+ users: z.array(linkedAccountSchema).min(1).describe('Users to fetch scores for'),
31
+ filter: z
32
+ .object({
33
+ type: z.enum([
34
+ 'quidli_score',
35
+ 'lens_score',
36
+ 'neynar_score',
37
+ 'ethos_twitter_reputation',
38
+ 'ethos_wallet_reputation',
39
+ ]),
40
+ minScore: z.number(),
41
+ })
42
+ .optional(),
43
+ };
44
+ export const scoresByAccountInputSchema = {
45
+ platform: z.enum([
46
+ 'wallet',
47
+ 'email',
48
+ 'phone',
49
+ 'telegram',
50
+ 'discord',
51
+ 'farcaster',
52
+ 'twitter',
53
+ 'github',
54
+ 'linkedin',
55
+ 'slack',
56
+ ]),
57
+ identifier: z.string().min(1),
58
+ };
59
+ export const scoresByUsernameInputSchema = {
60
+ username: z.string().min(1),
61
+ };
62
+ export const dropInputSchema = {
63
+ idempotencyKey: z.string().uuid(),
64
+ chainId: z.number().int(),
65
+ tokenContract: z.string().nullable().optional(),
66
+ recipients: z.array(linkedAccountSchema).min(1),
67
+ amountInWeiPerRecipient: z.string().optional().nullable(),
68
+ ignoreFailedRecipients: z.boolean().optional(),
69
+ };
70
+ export const agentPromptInputSchema = {
71
+ prompt: z.string().min(1).max(4000),
72
+ sessionId: z.string().uuid().optional(),
73
+ };
@@ -0,0 +1 @@
1
+ export declare function main(): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,9 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { createMcpServer } from './create-mcp-server.js';
3
+ import { loadConfig } from './config.js';
4
+ export async function main() {
5
+ const config = loadConfig();
6
+ const server = createMcpServer(config);
7
+ const transport = new StdioServerTransport();
8
+ await server.connect(transport);
9
+ }
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
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"];
4
+ export declare function registerTools(server: McpServer, client: ConnectClient): void;
@@ -0,0 +1,49 @@
1
+ import { agentPromptInputSchema, dropInputSchema, lookupInputSchema, scoresBatchInputSchema, scoresByAccountInputSchema, scoresByUsernameInputSchema, } from '../schemas.js';
2
+ const AGENT_TIMEOUT_MS = 65_000;
3
+ export const CONNECT_MCP_TOOL_NAMES = [
4
+ 'connect_get_price',
5
+ 'connect_lookup',
6
+ 'connect_scores_batch',
7
+ 'connect_scores_by_account',
8
+ 'connect_scores_by_username',
9
+ 'connect_drop',
10
+ 'connect_agent_prompt',
11
+ ];
12
+ export function registerTools(server, client) {
13
+ server.tool('connect_get_price', 'Get public x402 list prices for lookup, scores, and agent (reference only; live paywall amounts are in 402 responses).', {}, async () => client.request({ method: 'GET', path: '/price', authenticated: false }));
14
+ server.tool('connect_lookup', 'Resolve social identities to EVM and Solana wallet addresses. If status is processing, retry the same payload.', lookupInputSchema, async ({ recipients }) => client.request({
15
+ method: 'POST',
16
+ path: '/lookup',
17
+ body: { recipients },
18
+ }));
19
+ 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
+ method: 'POST',
21
+ path: '/scores',
22
+ body: { users, ...(filter ? { filter } : {}) },
23
+ }));
24
+ server.tool('connect_scores_by_account', 'Scores for a linked social account or wallet.', scoresByAccountInputSchema, async ({ platform, identifier }) => client.request({
25
+ method: 'GET',
26
+ path: `/scores/${encodeURIComponent(platform)}/${encodeURIComponent(identifier)}`,
27
+ }));
28
+ server.tool('connect_scores_by_username', 'Scores by Connect public username.', scoresByUsernameInputSchema, async ({ username }) => client.request({
29
+ method: 'GET',
30
+ path: `/scores/u/${encodeURIComponent(username.trim())}`,
31
+ }));
32
+ 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
+ method: 'POST',
34
+ path: '/drop',
35
+ body,
36
+ query: ignoreFailedRecipients === true
37
+ ? { ignoreFailedRecipients: 'true' }
38
+ : undefined,
39
+ }));
40
+ 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
+ method: 'POST',
42
+ path: '/agent',
43
+ body: {
44
+ prompt,
45
+ ...(sessionId ? { sessionId } : {}),
46
+ },
47
+ timeoutMs: AGENT_TIMEOUT_MS,
48
+ }));
49
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@quidli/connect-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Quidli Connect product APIs",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "quidli",
9
+ "connect",
10
+ "web3",
11
+ "identity",
12
+ "ai",
13
+ "agent"
14
+ ],
15
+ "homepage": "https://connect.quid.li",
16
+ "author": {
17
+ "name": "Quidli",
18
+ "url": "https://quid.xyz"
19
+ },
20
+ "type": "module",
21
+ "bin": {
22
+ "connect-mcp": "dist/index.js",
23
+ "connect-mcp-http": "dist/index-http.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "!dist/**/*.test.js",
28
+ "!dist/**/*.test.d.ts",
29
+ "!dist/**/*.test.js.map"
30
+ ],
31
+ "exports": {
32
+ ".": "./dist/index.js",
33
+ "./mount": "./dist/mount.js",
34
+ "./http": "./dist/http.js"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc && chmod +x dist/index.js dist/index-http.js",
38
+ "prepublishOnly": "npm run build",
39
+ "start": "node dist/index.js",
40
+ "start:http": "node dist/index-http.js",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "test:cov": "vitest run --coverage"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.25.2",
50
+ "express": "^5.2.1",
51
+ "zod": "^3.24.2"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.13.10",
55
+ "typescript": "^5.8.2",
56
+ "vitest": "^3.0.9"
57
+ },
58
+ "license": "MIT",
59
+ "publishConfig": {
60
+ "access": "public"
61
+ }
62
+ }