@askingahuman/mcp-server 2.0.5 → 2.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 +13 -5
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/tools/check-answer.js +2 -0
- package/dist/tools/discover.d.ts +1 -0
- package/dist/tools/discover.js +32 -3
- package/dist/tools/live.d.ts +1 -0
- package/dist/tools/live.js +21 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,12 +7,12 @@ Every request names a person. Find one with `list_humans` or `who_is_live`, then
|
|
|
7
7
|
## Tools
|
|
8
8
|
|
|
9
9
|
**Free**
|
|
10
|
-
- `list_humans`: browse expert profiles (name, title, specializations,
|
|
10
|
+
- `list_humans`: browse expert profiles (name, title, specializations, track record, and the credentials askingahuman has checked). Filter with `credential` (`license`, `degree`, `certification`, `employment`, `portfolio`) or `verified_only`.
|
|
11
11
|
- `who_is_live`: see who is live right now. It can wait up to 50 seconds for someone to come online.
|
|
12
12
|
- `ask_human`: send a free question (max 400 characters) to the person you choose. Not everyone takes free questions. Returns a `questionId`.
|
|
13
|
-
- `check_answer`: read the answer and any follow-up conversation.
|
|
13
|
+
- `check_answer`: read the answer and any follow-up conversation, with the signed answer receipt.
|
|
14
14
|
- `reply_to_human`: continue the conversation after an answer.
|
|
15
|
-
- `get_quote`: see what a live chat with a person would cost, what
|
|
15
|
+
- `get_quote`: see what a live chat with a person would cost, what the first block costs (held when you request), and how long it can run. Nothing is charged.
|
|
16
16
|
- `rate_human`: after a session ends or a question is answered, give the person a thumbs up or down (once). It feeds their thumbs-up rate, which `list_humans` shows with the rest of their track record.
|
|
17
17
|
- `submit_feedback`: send feedback about the service.
|
|
18
18
|
|
|
@@ -21,11 +21,19 @@ Every request names a person. Find one with `list_humans` or `who_is_live`, then
|
|
|
21
21
|
- `check_connection`: wait for the person to accept. You then join the chat automatically. Shows the paid time left.
|
|
22
22
|
- `read_messages` / `send_message`: talk in the chat. `read_messages` warns when less than 10 minutes of paid time remain.
|
|
23
23
|
- `extend_session`: add another hour, with the same cost approval as `connect_human`. Shows the new end time.
|
|
24
|
-
- `end_session`: leave the chat. Before the person accepts, this cancels the request and releases the hold.
|
|
24
|
+
- `end_session`: leave the chat and get the signed session receipt. Before the person accepts, this cancels the request and releases the hold.
|
|
25
25
|
|
|
26
26
|
### How live chat is priced
|
|
27
27
|
|
|
28
|
-
Each person sets their own price per hour of chat. The first hour
|
|
28
|
+
Each person sets their own price per hour of chat. The first hour costs that price or the platform's session minimum, whichever is higher (`get_quote` shows both); it is held when you request and charged in full once the person accepts. If they never accept, nothing is charged. `extend_session` buys one more hour at a time, charged in full. Paid time is not refunded if you end early, so the chat stays open until the paid time runs out or you end it. A session, extensions included, is capped by your spend limit, by the platform's $200 limit and by its 4-hour chat limit. All sales are final.
|
|
29
|
+
|
|
30
|
+
### Paying without a card
|
|
31
|
+
|
|
32
|
+
A key pays with the card saved on it at [askingahuman.com/developers](https://askingahuman.com/developers), or with prepaid credit. When the API offers them, its `402 Payment Required` answers also carry [x402](https://x402.org) (USDC on Base) and [MPP](https://mpp.dev) challenges: an agent with an x402 or MPP client can pay one, the payment becomes prepaid credit on the key, and the request goes through. The tools say when this applies. Details are on the [developers page](https://askingahuman.com/developers).
|
|
33
|
+
|
|
34
|
+
### Receipts
|
|
35
|
+
|
|
36
|
+
Answers and settled sessions come with a signed receipt (Ed25519 JWS). Verify it with the public keys at `https://api.askingahuman.com/.well-known/askingahuman-receipt-keys.json`.
|
|
29
37
|
|
|
30
38
|
## Setup
|
|
31
39
|
|
package/dist/server.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import type { McpConfig } from './config.js';
|
|
3
3
|
import type { LiveBridge } from './live/bridge.js';
|
|
4
|
-
export declare const SERVER_VERSION = "2.0
|
|
4
|
+
export declare const SERVER_VERSION = "2.1.0";
|
|
5
5
|
export declare function createMcpServer(opts: {
|
|
6
6
|
apiKey: string | undefined;
|
|
7
7
|
config: McpConfig;
|
package/dist/server.js
CHANGED
|
@@ -9,7 +9,7 @@ import { getQuoteTool, handleGetQuote, handleListHumans, listHumansTool } from '
|
|
|
9
9
|
import { handleWhoIsLive, whoIsLiveTool } from './tools/who-is-live.js';
|
|
10
10
|
import { handleRateHuman, rateHumanTool } from './tools/rate-human.js';
|
|
11
11
|
import { checkConnectionTool, connectHumanTool, endSessionTool, extendSessionTool, handleCheckConnection, handleConnectHuman, handleEndSession, handleExtendSession, handleReadMessages, handleSendMessage, readMessagesTool, sendMessageTool, } from './tools/live.js';
|
|
12
|
-
export const SERVER_VERSION = '2.0
|
|
12
|
+
export const SERVER_VERSION = '2.1.0';
|
|
13
13
|
const legacy = (fn) => async (args, ctx) => (await fn(args, ctx.apiKey, ctx.config.apiBase));
|
|
14
14
|
// Always available: free questions and discovery.
|
|
15
15
|
const CORE_TOOLS = [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { UNTRUSTED_NOTE, untrusted } from '../safety.js';
|
|
2
|
+
import { receiptNote } from './live.js';
|
|
2
3
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
4
|
export const checkAnswerTool = {
|
|
4
5
|
name: 'check_answer',
|
|
@@ -110,6 +111,7 @@ export async function handleCheckAnswer(args, apiKey, apiBase) {
|
|
|
110
111
|
`Question: ${data.text}\n${screenshotNote}\n` +
|
|
111
112
|
`Answer:\n${untrusted(String(data.answer ?? ''))}\n\n` +
|
|
112
113
|
`Answered at: ${data.answeredAt}` +
|
|
114
|
+
receiptNote(data.receipt) +
|
|
113
115
|
conversation +
|
|
114
116
|
`\n\nTo ask a follow-up or respond, use reply_to_human with question_id "${args.question_id}".`,
|
|
115
117
|
},
|
package/dist/tools/discover.d.ts
CHANGED
|
@@ -15,5 +15,6 @@ export declare function fetchQuote(args: Record<string, unknown> | undefined, ct
|
|
|
15
15
|
result: ToolResult;
|
|
16
16
|
}>;
|
|
17
17
|
export declare function blockPrice(pricing: unknown, mode?: unknown): string;
|
|
18
|
+
export declare const NO_PAYMENT_HINT: string;
|
|
18
19
|
export declare function describeQuote(q: Record<string, unknown>): string;
|
|
19
20
|
export declare function handleGetQuote(args: Record<string, unknown> | undefined, ctx: ToolContext): Promise<ToolResult>;
|
package/dist/tools/discover.js
CHANGED
|
@@ -10,10 +10,17 @@ export const listHumansTool = {
|
|
|
10
10
|
type: 'object',
|
|
11
11
|
properties: {
|
|
12
12
|
specialty: { type: 'string', description: 'Optional: only experts with this specialization.', maxLength: 100 },
|
|
13
|
+
credential: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
enum: ['license', 'degree', 'certification', 'employment', 'portfolio'],
|
|
16
|
+
description: 'Optional: only experts with a credential of this type that askingahuman has checked.',
|
|
17
|
+
},
|
|
18
|
+
verified_only: { type: 'boolean', description: 'Optional: only experts with at least one checked credential.' },
|
|
13
19
|
limit: { type: 'number', description: 'How many to return (1-25, default 10).' },
|
|
14
20
|
},
|
|
15
21
|
},
|
|
16
22
|
};
|
|
23
|
+
const CREDENTIAL_TYPES = ['license', 'degree', 'certification', 'employment', 'portfolio'];
|
|
17
24
|
// "live 12 h in the last 30 days · answers in about 4 min · accepts 90% of
|
|
18
25
|
// requests · 95% thumbs up". Rates only appear once the API has enough data.
|
|
19
26
|
function duration(seconds) {
|
|
@@ -43,6 +50,10 @@ export async function handleListHumans(args, ctx) {
|
|
|
43
50
|
const query = new URLSearchParams({ audience: 'bot', limit: String(limit) });
|
|
44
51
|
if (typeof args?.specialty === 'string' && args.specialty.trim())
|
|
45
52
|
query.set('specialty', args.specialty.trim().slice(0, 100));
|
|
53
|
+
if (typeof args?.credential === 'string' && CREDENTIAL_TYPES.includes(args.credential))
|
|
54
|
+
query.set('credential', args.credential);
|
|
55
|
+
if (args?.verified_only === true)
|
|
56
|
+
query.set('verifiedOnly', 'true');
|
|
46
57
|
try {
|
|
47
58
|
const res = await callApi(ctx, `/humans?${query}`);
|
|
48
59
|
if (!res.ok)
|
|
@@ -55,9 +66,16 @@ export async function handleListHumans(args, ctx) {
|
|
|
55
66
|
const lines = humans.map((h) => {
|
|
56
67
|
const specs = Array.isArray(h.specializations) ? h.specializations.map((s) => short(s, 40)).join(', ') : '';
|
|
57
68
|
const record = trackRecord(h.stats);
|
|
69
|
+
const creds = Array.isArray(h.credentials)
|
|
70
|
+
? h.credentials
|
|
71
|
+
.map((c) => [short(c.title, 60), c.issuer ? short(c.issuer, 60) : ''].filter(Boolean).join(', '))
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.join('; ')
|
|
74
|
+
: '';
|
|
58
75
|
return `- ${short(h.name, 60)} (humanId: ${short(h.humanId, 80)})${h.title ? ` — ${short(h.title, 80)}` : ''}` +
|
|
59
76
|
(specs ? `\n Specializations: ${specs}` : '') +
|
|
60
|
-
(record ? `\n Track record: ${record}` : '')
|
|
77
|
+
(record ? `\n Track record: ${record}` : '') +
|
|
78
|
+
(creds ? `\n Checked credentials: ${creds}` : '');
|
|
61
79
|
});
|
|
62
80
|
return textResult(`${humans.length} expert${humans.length === 1 ? '' : 's'}:\n${lines.join('\n')}\n\n` +
|
|
63
81
|
'Prices and live status are not listed here; use get_quote with a humanId to see them.');
|
|
@@ -123,6 +141,17 @@ function hours(minutes) {
|
|
|
123
141
|
const n = Number(minutes);
|
|
124
142
|
return n % 60 === 0 ? `${n / 60} hour${n === 60 ? '' : 's'}` : `${n} minutes`;
|
|
125
143
|
}
|
|
144
|
+
// Told to the agent when its key can't pay yet.
|
|
145
|
+
export const NO_PAYMENT_HINT = 'This API key has no card or prepaid credit yet. Add a card at https://askingahuman.com/developers, ' +
|
|
146
|
+
'or, when the 402 from the API offers x402 (USDC on Base) or MPP, pay it: the payment becomes prepaid credit on the key.';
|
|
147
|
+
// The first block is charged at least the session minimum ($20); extensions at the block price.
|
|
148
|
+
function firstCharge(q) {
|
|
149
|
+
const first = Number(q.firstChargeCents ?? q.holdAmountCents);
|
|
150
|
+
const block = Number(q.pricing?.blockPriceCents);
|
|
151
|
+
const minimum = Number(q.sessionMinimumCents);
|
|
152
|
+
const floor = Number.isFinite(block) && first > block && minimum > 0 ? ` (the ${dollars(minimum)} session minimum)` : '';
|
|
153
|
+
return `The first block costs ${dollars(first)}${floor}, held when you request; each extension costs ${dollars(block)}.`;
|
|
154
|
+
}
|
|
126
155
|
export function describeQuote(q) {
|
|
127
156
|
const human = q.human;
|
|
128
157
|
const who = human ? `${cleanIncoming(String(human.name || 'This person'), 60)} (humanId: ${human.humanId})` : 'This person';
|
|
@@ -132,11 +161,11 @@ export function describeQuote(q) {
|
|
|
132
161
|
}
|
|
133
162
|
return (`${who} is ${human?.live ? 'live now' : 'offline; a chat request waits up to 24 hours for them'}.\n` +
|
|
134
163
|
`Price: ${blockPrice(q.pricing, q.mode)}, charged in full once they accept (nothing if they never do). ` +
|
|
135
|
-
`${
|
|
164
|
+
`${firstCharge(q)}\n` +
|
|
136
165
|
`Length: one hour to start; extend_session adds another hour, up to ${hours(q.maxSessionMinutes)} ` +
|
|
137
166
|
`within your ${dollars(q.spendLimitCents)} spend limit. Ending early does not refund paid time.\n` +
|
|
138
167
|
`All sales are final.` +
|
|
139
|
-
(q.paymentReady === false ? `\
|
|
168
|
+
(q.paymentReady === false ? `\n${NO_PAYMENT_HINT}` : ''));
|
|
140
169
|
}
|
|
141
170
|
export async function handleGetQuote(args, ctx) {
|
|
142
171
|
const res = await fetchQuote(args, ctx);
|
package/dist/tools/live.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare function handleSendMessage(args: Record<string, unknown> | undefi
|
|
|
12
12
|
export declare const readMessagesTool: Tool;
|
|
13
13
|
export declare function handleReadMessages(args: Record<string, unknown> | undefined, ctx: ToolContext): Promise<ToolResult>;
|
|
14
14
|
export declare const endSessionTool: Tool;
|
|
15
|
+
export declare const receiptNote: (receipt: unknown) => string;
|
|
15
16
|
export declare function endSessionViaApi(ctx: Pick<ToolContext, 'apiKey' | 'config'>, sessionId: string): Promise<void>;
|
|
16
17
|
export declare function handleEndSession(args: Record<string, unknown> | undefined, ctx: ToolContext): Promise<ToolResult>;
|
|
17
18
|
export declare const extendSessionTool: Tool;
|
package/dist/tools/live.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ApiError, apiErrorText, callApi, dollars, errorResult, textResult } from '../context.js';
|
|
2
2
|
import { LiveChatError } from '../live/bridge.js';
|
|
3
3
|
import { UNTRUSTED_NOTE, checkOutgoing, untrusted } from '../safety.js';
|
|
4
|
-
import { blockPrice, describeQuote, fetchQuote, humanIdFrom, HUMAN_ID_REQUIRED, spendLimitCents } from './discover.js';
|
|
4
|
+
import { blockPrice, describeQuote, fetchQuote, humanIdFrom, HUMAN_ID_REQUIRED, NO_PAYMENT_HINT, spendLimitCents } from './discover.js';
|
|
5
5
|
const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
6
|
export const MAX_CHAT_CHARS = 1000;
|
|
7
7
|
const MAX_WAIT_SECONDS = 50;
|
|
@@ -118,7 +118,7 @@ export async function handleConnectHuman(args, ctx) {
|
|
|
118
118
|
return textResult(`Not started: ${why}${/charged/i.test(why) ? '' : ' Nothing was charged.'}`);
|
|
119
119
|
}
|
|
120
120
|
if (res.status === 402) {
|
|
121
|
-
return errorResult(
|
|
121
|
+
return errorResult(`Payment required. ${paymentHint(res.data)} Nothing was charged.`);
|
|
122
122
|
}
|
|
123
123
|
if (!res.ok)
|
|
124
124
|
return errorResult(`${apiErrorText(res, 'Failed to request a chat')}. Manage your key at https://askingahuman.com/developers`);
|
|
@@ -128,7 +128,7 @@ export async function handleConnectHuman(args, ctx) {
|
|
|
128
128
|
const waits = typeof res.data.waitsUntil === 'string' ? ` (until ${res.data.waitsUntil})` : '';
|
|
129
129
|
return textResult(`Chat requested. The person has been notified${human.live ? '' : ` and the request waits for them up to 24 hours${waits}`}.\n` +
|
|
130
130
|
`Session ID: ${sessionId}\n` +
|
|
131
|
-
`Price: ${blockPrice(res.data.pricing, 'message')}; ${dollars(res.data.holdAmountCents)} held now, charged in full once they accept.\n\n` +
|
|
131
|
+
`Price: ${blockPrice(res.data.pricing, 'message')}; ${dollars(res.data.holdAmountCents)} held now for the first block, charged in full once they accept.\n\n` +
|
|
132
132
|
`Next: call check_connection with session_id "${sessionId}" and wait_seconds 45 until they accept. ` +
|
|
133
133
|
`To cancel before they accept, call end_session (the hold is released).`);
|
|
134
134
|
}
|
|
@@ -298,6 +298,19 @@ export const endSessionTool = {
|
|
|
298
298
|
required: ['session_id'],
|
|
299
299
|
},
|
|
300
300
|
};
|
|
301
|
+
// A 402: the key has no card or credit. The API offers x402 and MPP
|
|
302
|
+
// challenges (data.payWith) when they are turned on.
|
|
303
|
+
function paymentHint(data) {
|
|
304
|
+
return typeof data.payWith === 'string'
|
|
305
|
+
? 'This key has no card or credit. The 402 also accepts x402 (USDC on Base) or MPP; a payment becomes prepaid credit on the key. ' +
|
|
306
|
+
'Pay it with an x402 or MPP client, or add a card at https://askingahuman.com/developers.'
|
|
307
|
+
: NO_PAYMENT_HINT;
|
|
308
|
+
}
|
|
309
|
+
// Settled sessions carry a signed receipt (Ed25519 JWS; public keys at
|
|
310
|
+
// https://api.askingahuman.com/.well-known/askingahuman-receipt-keys.json).
|
|
311
|
+
export const receiptNote = (receipt) => typeof receipt === 'string' && receipt
|
|
312
|
+
? `\nSigned receipt (verify with the keys at https://api.askingahuman.com/.well-known/askingahuman-receipt-keys.json):\n${receipt}`
|
|
313
|
+
: '';
|
|
301
314
|
// After a session the person took (it was charged), invite a thumbs up/down.
|
|
302
315
|
const rateHint = (amountCents) => Number(amountCents) > 0 ? '\nDid they help? Give a thumbs up or down with rate_human and this session_id.' : '';
|
|
303
316
|
export async function endSessionViaApi(ctx, sessionId) {
|
|
@@ -316,8 +329,9 @@ export async function handleEndSession(args, ctx) {
|
|
|
316
329
|
else
|
|
317
330
|
await endSessionViaApi(ctx, sessionId);
|
|
318
331
|
const res = await callApi(ctx, `/api/connect/${sessionId}`);
|
|
319
|
-
if (res.ok && res.data.status === 'ENDED')
|
|
320
|
-
return textResult(`Session ended. ${chargedText(res.data.amountCents)}${rateHint(res.data.amountCents)}`);
|
|
332
|
+
if (res.ok && res.data.status === 'ENDED') {
|
|
333
|
+
return textResult(`Session ended. ${chargedText(res.data.amountCents)}${receiptNote(res.data.receipt)}${rateHint(res.data.amountCents)}`);
|
|
334
|
+
}
|
|
321
335
|
return textResult('Session ended. Use check_connection in a moment to see the final charge.');
|
|
322
336
|
}
|
|
323
337
|
catch (err) {
|
|
@@ -358,13 +372,13 @@ export async function handleExtendSession(args, ctx) {
|
|
|
358
372
|
const pricing = (data.pricing || {});
|
|
359
373
|
const blocks = Number(data.blocksPurchased ?? 1);
|
|
360
374
|
const stop = await approve(ctx, `Your AI agent wants to extend a live chat on askingahuman by one more ${blockSpan(data.blockMinutes)}.\n\n` +
|
|
361
|
-
`Cost: ${dollars(pricing.blockPriceCents)}, charged in full now. Session total: ${dollars((
|
|
375
|
+
`Cost: ${dollars(pricing.blockPriceCents)}, charged in full now. Session total: ${dollars(Number(pricing.firstChargeCents ?? pricing.blockPriceCents) + blocks * Number(pricing.blockPriceCents))}.\n` +
|
|
362
376
|
`All sales are final.\n\nApprove this extension?`, 'the chat was not extended');
|
|
363
377
|
if (stop)
|
|
364
378
|
return stop;
|
|
365
379
|
const res = await callApi(ctx, `/api/connect/${sessionId}/extend`, { method: 'POST', body: {} });
|
|
366
380
|
if (res.status === 402) {
|
|
367
|
-
return errorResult(
|
|
381
|
+
return errorResult(`Payment required: the extra block could not be paid. ${paymentHint(res.data)} The chat was not extended.`);
|
|
368
382
|
}
|
|
369
383
|
if (res.status === 409) {
|
|
370
384
|
const why = typeof res.data.message === 'string' ? res.data.message : apiErrorText(res, 'Cannot extend');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askingahuman/mcp-server",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"mcpName": "com.askingahuman/mcp-server",
|
|
5
5
|
"description": "MCP server for askingahuman.com: let AI agents reach real people, with free questions and capped, paid live chats. mcprush.com-cd4a86101a8a6fd64075c4f5",
|
|
6
6
|
"type": "module",
|