psychosynth-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,82 @@
1
+ # psychosynth-mcp
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server that makes the **Psychosynth**
4
+ synthetic psychometric data marketplace discoverable and usable by AI agents
5
+ (Claude, OpenAI, and any MCP-compatible client).
6
+
7
+ It exposes the public API as four tools: browse the catalog, fetch a free
8
+ deterministic preview, get an x402 price quote, and run a paid query. Discovery
9
+ tools are free and need no wallet; paid queries settle in USDC on Base using a
10
+ wallet **you** configure.
11
+
12
+ ## Tools
13
+
14
+ | Tool | Cost | Description |
15
+ | --- | --- | --- |
16
+ | `list_products` | free | List available data products with slugs, per-query prices, and bulk pack tiers. |
17
+ | `preview_records` | free | Deterministic sample of a product's records — same rows every time, so you can verify quality before paying. |
18
+ | `get_quote` | free | The x402 payment quote (price + accepted tiers) for a paid query, without paying. |
19
+ | `query_records` | paid | Full records. Pays automatically over x402 using `BUYER_PRIVATE_KEY`; if unset, returns the quote instead. Add `tier` for a bulk pack. |
20
+
21
+ ## Install & configure
22
+
23
+ The server is distributed on npm and run with `npx`. Add it to your MCP client.
24
+
25
+ **Claude Desktop** (`claude_desktop_config.json`):
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "psychosynth": {
31
+ "command": "npx",
32
+ "args": ["-y", "psychosynth-mcp"],
33
+ "env": {
34
+ "PSYCHOSYNTH_API_URL": "https://your-deployment.example.com",
35
+ "BUYER_PRIVATE_KEY": "0xYOUR_BASE_WALLET_KEY"
36
+ }
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ ### Environment variables
43
+
44
+ | Variable | Required | Purpose |
45
+ | --- | --- | --- |
46
+ | `PSYCHOSYNTH_API_URL` | no | Base URL of the Psychosynth deployment. Defaults to `http://localhost:3000`. |
47
+ | `BUYER_PRIVATE_KEY` | no | Base (EVM) wallet key used to pay for queries. **Omit to use only the free tools.** |
48
+ | `BASE_RPC_URL` | no | Custom Base RPC. Defaults to `https://mainnet.base.org`. |
49
+
50
+ ## Payment & safety
51
+
52
+ - `list_products`, `preview_records`, and `get_quote` never touch a wallet.
53
+ - `query_records` pays **only** with the key in `BUYER_PRIVATE_KEY`, which you
54
+ set. The server holds no other key and never moves funds on its own — if no
55
+ wallet is configured, a paid query returns the quote instead of paying.
56
+ - Only keep enough USDC/ETH on the buyer wallet for expected spend. Treat the
57
+ key as a secret; never commit it.
58
+
59
+ ## How payment works
60
+
61
+ `query_records` performs the standard x402 handshake: it requests the paid
62
+ endpoint, receives a `402` quote, signs an EIP-3009 `TransferWithAuthorization`
63
+ with your wallet (no gas on your side — the seller settles), and retries with
64
+ the signed authorization. Base mainnet clock drift is compensated automatically
65
+ so the authorization window is accepted.
66
+
67
+ ## Local development
68
+
69
+ ```bash
70
+ npm install
71
+ npm run build
72
+ PSYCHOSYNTH_API_URL=http://localhost:3000 node dist/index.js
73
+ ```
74
+
75
+ The server speaks MCP over stdio; run it from an MCP client rather than
76
+ interacting directly.
77
+
78
+ ## Registry
79
+
80
+ `server.json` is an [MCP registry](https://github.com/modelcontextprotocol/registry)
81
+ manifest. Before publishing, replace `OWNER` with your GitHub namespace and
82
+ publish `psychosynth-mcp` to npm.
package/dist/api.js ADDED
@@ -0,0 +1,27 @@
1
+ // Thin HTTP client for the Psychosynth public API. Every endpoint used here
2
+ // (catalog, preview, quote) is free and unauthenticated — no wallet involved.
3
+ import { readBodySafe } from './util.js';
4
+ export const API_BASE = (process.env.PSYCHOSYNTH_API_URL || 'https://psychosynth.vercel.app').replace(/\/+$/, '');
5
+ export function buildQueryUrl(slug, filters) {
6
+ const url = new URL(`${API_BASE}/api/v1/query/${encodeURIComponent(slug)}`);
7
+ for (const [k, v] of Object.entries(filters ?? {})) {
8
+ if (v != null && v !== '')
9
+ url.searchParams.set(k, String(v));
10
+ }
11
+ return url.toString();
12
+ }
13
+ export async function getJson(url) {
14
+ const res = await fetch(url, { headers: { accept: 'application/json' } });
15
+ return { status: res.status, body: await readBodySafe(res) };
16
+ }
17
+ export async function listProducts() {
18
+ return getJson(`${API_BASE}/api/v1/products`);
19
+ }
20
+ export async function previewRecords(slug) {
21
+ return getJson(`${API_BASE}/api/v1/preview/${encodeURIComponent(slug)}`);
22
+ }
23
+ // Hitting the paid endpoint with no X-PAYMENT header returns the 402 quote
24
+ // (price + available pack tiers). Pure discovery: no wallet, no money moved.
25
+ export async function getQuote(slug, filters) {
26
+ return getJson(buildQueryUrl(slug, filters));
27
+ }
package/dist/index.js ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { API_BASE, listProducts, previewRecords, getQuote, buildQueryUrl } from './api.js';
6
+ import { payingFetch, hasBuyerWallet, buyerAddress } from './payment.js';
7
+ const server = new McpServer({ name: 'psychosynth', version: '0.1.0' });
8
+ // MCP tool results are text content blocks; JSON is stringified for the model.
9
+ function result(payload) {
10
+ const text = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
11
+ return { content: [{ type: 'text', text }] };
12
+ }
13
+ // Shared input schema for the query-style tools. `filters` carries any
14
+ // product-specific parameters (each product lists its allowed ones in
15
+ // list_products); `tier` and `limit` are broken out for convenience.
16
+ const querySchema = {
17
+ slug: z.string().describe("Product slug from list_products, e.g. 'behavioral-response-library'"),
18
+ tier: z
19
+ .string()
20
+ .optional()
21
+ .describe("Optional pack tier slug (e.g. 'pack-5k') for a bulk single-call result; omit for base per-query pricing"),
22
+ limit: z.string().optional().describe('Optional max number of records to return'),
23
+ filters: z
24
+ .record(z.string())
25
+ .optional()
26
+ .describe("Product-specific query filters, e.g. { category: 'trading' } or { mbti_label: 'INTJ' }. See each product's allowed parameters in list_products."),
27
+ };
28
+ function collectFilters(args) {
29
+ return {
30
+ ...(args.filters ?? {}),
31
+ ...(args.tier ? { tier: args.tier } : {}),
32
+ ...(args.limit ? { limit: args.limit } : {}),
33
+ };
34
+ }
35
+ server.registerTool('list_products', {
36
+ title: 'List products',
37
+ description: 'List the Psychosynth data products available for purchase (free, no wallet). Returns each product\'s slug, description, per-query price, and any bulk pack tiers. Start here to discover what can be queried.',
38
+ inputSchema: {},
39
+ }, async () => {
40
+ const { status, body } = await listProducts();
41
+ if (status !== 200)
42
+ return result({ error: 'catalog request failed', status, body });
43
+ return result(body);
44
+ });
45
+ server.registerTool('preview_records', {
46
+ title: 'Preview records (free)',
47
+ description: 'Fetch a free, deterministic sample of a product\'s records so you can verify shape and quality before paying. The same product always returns the same preview rows. Provide a product slug from list_products.',
48
+ inputSchema: { slug: z.string().describe("Product slug, e.g. 'behavioral-response-library'") },
49
+ }, async ({ slug }) => {
50
+ const { status, body } = await previewRecords(slug);
51
+ if (status !== 200)
52
+ return result({ error: 'preview request failed', status, body });
53
+ return result(body);
54
+ });
55
+ server.registerTool('get_quote', {
56
+ title: 'Get price quote (free)',
57
+ description: 'Get the x402 payment quote for a paid query WITHOUT paying — returns the price, accepted payment details, and available pack tiers. No wallet required. Use this to see the exact cost before calling query_records.',
58
+ inputSchema: querySchema,
59
+ }, async (args) => {
60
+ const { status, body } = await getQuote(args.slug, collectFilters(args));
61
+ return result({
62
+ note: status === 402 ? '402 quote — payment required to fetch records' : `status ${status}`,
63
+ quote: body,
64
+ });
65
+ });
66
+ server.registerTool('query_records', {
67
+ title: 'Query records (paid via x402)',
68
+ description: 'Run a paid query and return the full records. Pays automatically over x402 (USDC on Base) using the wallet in the BUYER_PRIVATE_KEY environment variable. If no wallet is configured, returns the price quote instead so you can review the cost first. Add `tier` for a bulk pack.',
69
+ inputSchema: querySchema,
70
+ }, async (args) => {
71
+ const filters = collectFilters(args);
72
+ if (!hasBuyerWallet()) {
73
+ const { status, body } = await getQuote(args.slug, filters);
74
+ return result({
75
+ error: 'No buyer wallet configured. Set BUYER_PRIVATE_KEY to enable paid queries.',
76
+ status,
77
+ quote: body,
78
+ });
79
+ }
80
+ try {
81
+ const { status, body } = await payingFetch(buildQueryUrl(args.slug, filters));
82
+ if (status !== 200)
83
+ return result({ error: 'paid query failed', status, body });
84
+ return result(body);
85
+ }
86
+ catch (e) {
87
+ return result({ error: 'payment/query error', message: e?.message ?? String(e) });
88
+ }
89
+ });
90
+ async function main() {
91
+ const transport = new StdioServerTransport();
92
+ await server.connect(transport);
93
+ // stdout is reserved for the MCP protocol; log to stderr only.
94
+ console.error(`psychosynth-mcp connected. API=${API_BASE} wallet=${hasBuyerWallet() ? buyerAddress() : 'not configured (free tools only)'}`);
95
+ }
96
+ main().catch((e) => {
97
+ console.error('fatal:', e);
98
+ process.exit(1);
99
+ });
@@ -0,0 +1,53 @@
1
+ import { createPublicClient, createWalletClient, http, publicActions } from 'viem';
2
+ import { privateKeyToAccount } from 'viem/accounts';
3
+ import { base } from 'viem/chains';
4
+ import { wrapFetchWithPayment } from 'x402-fetch';
5
+ import { readBodySafe } from './util.js';
6
+ const RPC = process.env.BASE_RPC_URL || 'https://mainnet.base.org';
7
+ function buyerKey() {
8
+ const pk = process.env.BUYER_PRIVATE_KEY;
9
+ if (!pk)
10
+ return undefined;
11
+ return (pk.startsWith('0x') ? pk : `0x${pk}`);
12
+ }
13
+ export function hasBuyerWallet() {
14
+ return !!buyerKey();
15
+ }
16
+ export function buyerAddress() {
17
+ const pk = buyerKey();
18
+ if (!pk)
19
+ return null;
20
+ try {
21
+ return privateKeyToAccount(pk).address;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ // Paid fetch using the operator's OWN wallet key (BUYER_PRIVATE_KEY). The server
28
+ // never holds any other key and never moves funds on its own — a paid query
29
+ // only happens when the operator has explicitly configured their wallet.
30
+ // Mirrors scripts/buyer-test.ts, including Base clock-drift compensation, which
31
+ // keeps the EIP-3009 validAfter/validBefore window inside what the chain accepts.
32
+ export async function payingFetch(url) {
33
+ const pk = buyerKey();
34
+ if (!pk)
35
+ throw new Error('BUYER_PRIVATE_KEY not set');
36
+ const publicClient = createPublicClient({ chain: base, transport: http(RPC) });
37
+ const block = await publicClient.getBlock();
38
+ const driftSeconds = Math.floor(Date.now() / 1000) - Number(block.timestamp);
39
+ const account = privateKeyToAccount(pk);
40
+ const client = createWalletClient({ account, chain: base, transport: http(RPC) }).extend(publicActions);
41
+ const fetchWithPay = wrapFetchWithPayment(fetch, client);
42
+ // Compensate for local/chain clock drift (+5m safety) so validAfter is in the
43
+ // past when the settlement contract checks it. Restored immediately after.
44
+ const originalNow = Date.now;
45
+ Date.now = () => originalNow() - (driftSeconds + 300) * 1000;
46
+ try {
47
+ const res = await fetchWithPay(url);
48
+ return { status: res.status, body: await readBodySafe(res) };
49
+ }
50
+ finally {
51
+ Date.now = originalNow;
52
+ }
53
+ }
package/dist/util.js ADDED
@@ -0,0 +1,16 @@
1
+ // Read a fetch Response body as JSON, falling back to text, then null.
2
+ // Used for both free API calls and paid (x402) responses, which may return
3
+ // JSON on success and JSON or text on error/402.
4
+ export async function readBodySafe(res) {
5
+ try {
6
+ return await res.json();
7
+ }
8
+ catch {
9
+ try {
10
+ return await res.text();
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "psychosynth-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for the Psychosynth synthetic psychometric data marketplace — discover the catalog, fetch free deterministic previews, get x402 price quotes, and run paid queries with your own wallet.",
5
+ "type": "module",
6
+ "bin": {
7
+ "psychosynth-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "server.json"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/index.js",
17
+ "dev": "tsc --watch"
18
+ },
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "modelcontextprotocol",
25
+ "x402",
26
+ "psychometric",
27
+ "synthetic-data",
28
+ "agents"
29
+ ],
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.12.0",
33
+ "viem": "^2.55.0",
34
+ "x402-fetch": "^1.2.0",
35
+ "zod": "^3.25.76"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20",
39
+ "typescript": "^5"
40
+ }
41
+ }
package/server.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
3
+ "name": "io.github.3esign/psychosynth",
4
+ "description": "Discover and query Psychosynth synthetic psychometric data (Big Five profiles, cognitive biases, profile-conditioned behavioral responses) over the x402 protocol. Free catalog, free deterministic previews, and price quotes; paid queries settle in USDC on Base using your own wallet.",
5
+ "repository": {
6
+ "url": "https://github.com/3esign/cfaces",
7
+ "source": "github"
8
+ },
9
+ "version": "0.1.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "psychosynth-mcp",
14
+ "version": "0.1.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ },
18
+ "environmentVariables": [
19
+ {
20
+ "name": "PSYCHOSYNTH_API_URL",
21
+ "description": "Base URL of the Psychosynth API deployment (e.g. https://your-app.vercel.app). Defaults to http://localhost:3000 for local development.",
22
+ "isRequired": false,
23
+ "isSecret": false
24
+ },
25
+ {
26
+ "name": "BUYER_PRIVATE_KEY",
27
+ "description": "Base (EVM) wallet private key used to pay for queries via x402. Omit to use only the free catalog, preview, and quote tools.",
28
+ "isRequired": false,
29
+ "isSecret": true
30
+ },
31
+ {
32
+ "name": "BASE_RPC_URL",
33
+ "description": "Optional custom Base mainnet RPC endpoint. Defaults to https://mainnet.base.org.",
34
+ "isRequired": false,
35
+ "isSecret": false
36
+ }
37
+ ]
38
+ }
39
+ ]
40
+ }