@askingahuman/mcp-server 2.0.4 → 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 +14 -5
- package/dist/server.d.ts +1 -1
- package/dist/server.js +3 -1
- package/dist/tools/check-answer.js +2 -0
- package/dist/tools/discover.d.ts +2 -0
- package/dist/tools/discover.js +61 -6
- package/dist/tools/live.d.ts +1 -0
- package/dist/tools/live.js +24 -8
- package/dist/tools/rate-human.d.ts +4 -0
- package/dist/tools/rate-human.js +43 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,12 +7,13 @@ 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
|
+
- `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.
|
|
16
17
|
- `submit_feedback`: send feedback about the service.
|
|
17
18
|
|
|
18
19
|
**Paid live chat** (local server only, text only)
|
|
@@ -20,11 +21,19 @@ Every request names a person. Find one with `list_humans` or `who_is_live`, then
|
|
|
20
21
|
- `check_connection`: wait for the person to accept. You then join the chat automatically. Shows the paid time left.
|
|
21
22
|
- `read_messages` / `send_message`: talk in the chat. `read_messages` warns when less than 10 minutes of paid time remain.
|
|
22
23
|
- `extend_session`: add another hour, with the same cost approval as `connect_human`. Shows the new end time.
|
|
23
|
-
- `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.
|
|
24
25
|
|
|
25
26
|
### How live chat is priced
|
|
26
27
|
|
|
27
|
-
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`.
|
|
28
37
|
|
|
29
38
|
## Setup
|
|
30
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
|
@@ -7,8 +7,9 @@ import { replyToHumanTool, handleReplyToHuman } from './tools/reply-to-human.js'
|
|
|
7
7
|
import { submitFeedbackTool, handleSubmitFeedback } from './tools/submit-feedback.js';
|
|
8
8
|
import { getQuoteTool, handleGetQuote, handleListHumans, listHumansTool } from './tools/discover.js';
|
|
9
9
|
import { handleWhoIsLive, whoIsLiveTool } from './tools/who-is-live.js';
|
|
10
|
+
import { handleRateHuman, rateHumanTool } from './tools/rate-human.js';
|
|
10
11
|
import { checkConnectionTool, connectHumanTool, endSessionTool, extendSessionTool, handleCheckConnection, handleConnectHuman, handleEndSession, handleExtendSession, handleReadMessages, handleSendMessage, readMessagesTool, sendMessageTool, } from './tools/live.js';
|
|
11
|
-
export const SERVER_VERSION = '2.0
|
|
12
|
+
export const SERVER_VERSION = '2.1.0';
|
|
12
13
|
const legacy = (fn) => async (args, ctx) => (await fn(args, ctx.apiKey, ctx.config.apiBase));
|
|
13
14
|
// Always available: free questions and discovery.
|
|
14
15
|
const CORE_TOOLS = [
|
|
@@ -18,6 +19,7 @@ const CORE_TOOLS = [
|
|
|
18
19
|
[listHumansTool, handleListHumans],
|
|
19
20
|
[whoIsLiveTool, handleWhoIsLive],
|
|
20
21
|
[getQuoteTool, handleGetQuote],
|
|
22
|
+
[rateHumanTool, handleRateHuman],
|
|
21
23
|
[submitFeedbackTool, legacy(handleSubmitFeedback)],
|
|
22
24
|
];
|
|
23
25
|
// Paid live chat: only where this process can hold the chat open (stdio).
|
|
@@ -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { ToolContext, ToolResult } from '../context.js';
|
|
3
3
|
export declare const listHumansTool: Tool;
|
|
4
|
+
export declare function trackRecord(stats: unknown): string;
|
|
4
5
|
export declare function handleListHumans(args: Record<string, unknown> | undefined, ctx: ToolContext): Promise<ToolResult>;
|
|
5
6
|
export declare const getQuoteTool: Tool;
|
|
6
7
|
export declare function humanIdFrom(args: Record<string, unknown> | undefined): string | null;
|
|
@@ -14,5 +15,6 @@ export declare function fetchQuote(args: Record<string, unknown> | undefined, ct
|
|
|
14
15
|
result: ToolResult;
|
|
15
16
|
}>;
|
|
16
17
|
export declare function blockPrice(pricing: unknown, mode?: unknown): string;
|
|
18
|
+
export declare const NO_PAYMENT_HINT: string;
|
|
17
19
|
export declare function describeQuote(q: Record<string, unknown>): string;
|
|
18
20
|
export declare function handleGetQuote(args: Record<string, unknown> | undefined, ctx: ToolContext): Promise<ToolResult>;
|
package/dist/tools/discover.js
CHANGED
|
@@ -2,22 +2,58 @@ import { ApiError, apiErrorText, callApi, dollars, errorResult, textResult } fro
|
|
|
2
2
|
import { cleanIncoming } from '../safety.js';
|
|
3
3
|
export const listHumansTool = {
|
|
4
4
|
name: 'list_humans',
|
|
5
|
-
description: 'List the reviewed experts on askingahuman.com: name, title, specializations,
|
|
5
|
+
description: 'List the reviewed experts on askingahuman.com: name, title, specializations, and track record (hours live, ' +
|
|
6
|
+
'how fast they answer, how often they accept, and how often agents gave a thumbs up). ' +
|
|
6
7
|
'Every request names a person: use a humanId from here with get_quote, connect_human or ask_human. ' +
|
|
7
8
|
'Profiles are public summaries only; no contact details are shared.',
|
|
8
9
|
inputSchema: {
|
|
9
10
|
type: 'object',
|
|
10
11
|
properties: {
|
|
11
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.' },
|
|
12
19
|
limit: { type: 'number', description: 'How many to return (1-25, default 10).' },
|
|
13
20
|
},
|
|
14
21
|
},
|
|
15
22
|
};
|
|
23
|
+
const CREDENTIAL_TYPES = ['license', 'degree', 'certification', 'employment', 'portfolio'];
|
|
24
|
+
// "live 12 h in the last 30 days · answers in about 4 min · accepts 90% of
|
|
25
|
+
// requests · 95% thumbs up". Rates only appear once the API has enough data.
|
|
26
|
+
function duration(seconds) {
|
|
27
|
+
if (seconds < 90)
|
|
28
|
+
return `${Math.max(1, Math.round(seconds))} sec`;
|
|
29
|
+
if (seconds < 5400)
|
|
30
|
+
return `${Math.round(seconds / 60)} min`;
|
|
31
|
+
return `${Math.round((seconds / 3600) * 10) / 10} hours`;
|
|
32
|
+
}
|
|
33
|
+
export function trackRecord(stats) {
|
|
34
|
+
const s = (stats || {});
|
|
35
|
+
const parts = [];
|
|
36
|
+
if (typeof s.interactions === 'string' && s.interactions !== '0')
|
|
37
|
+
parts.push(`${s.interactions} sessions and answers`);
|
|
38
|
+
if (typeof s.liveHours30d === 'number')
|
|
39
|
+
parts.push(`live ${s.liveHours30d} h in the last 30 days`);
|
|
40
|
+
if (typeof s.medianResponseSeconds === 'number')
|
|
41
|
+
parts.push(`answers in about ${duration(s.medianResponseSeconds)}`);
|
|
42
|
+
if (typeof s.acceptanceRate === 'number')
|
|
43
|
+
parts.push(`accepts ${Math.round(s.acceptanceRate * 100)}% of requests`);
|
|
44
|
+
if (typeof s.satisfactionRate === 'number')
|
|
45
|
+
parts.push(`${Math.round(s.satisfactionRate * 100)}% thumbs up from agents`);
|
|
46
|
+
return parts.join(' · ');
|
|
47
|
+
}
|
|
16
48
|
export async function handleListHumans(args, ctx) {
|
|
17
49
|
const limit = Math.min(25, Math.max(1, Math.floor(Number(args?.limit) || 10)));
|
|
18
50
|
const query = new URLSearchParams({ audience: 'bot', limit: String(limit) });
|
|
19
51
|
if (typeof args?.specialty === 'string' && args.specialty.trim())
|
|
20
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');
|
|
21
57
|
try {
|
|
22
58
|
const res = await callApi(ctx, `/humans?${query}`);
|
|
23
59
|
if (!res.ok)
|
|
@@ -29,9 +65,17 @@ export async function handleListHumans(args, ctx) {
|
|
|
29
65
|
const short = (v, n) => cleanIncoming(String(v ?? ''), n).replace(/\s+/g, ' ').trim();
|
|
30
66
|
const lines = humans.map((h) => {
|
|
31
67
|
const specs = Array.isArray(h.specializations) ? h.specializations.map((s) => short(s, 40)).join(', ') : '';
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
: '';
|
|
75
|
+
return `- ${short(h.name, 60)} (humanId: ${short(h.humanId, 80)})${h.title ? ` — ${short(h.title, 80)}` : ''}` +
|
|
76
|
+
(specs ? `\n Specializations: ${specs}` : '') +
|
|
77
|
+
(record ? `\n Track record: ${record}` : '') +
|
|
78
|
+
(creds ? `\n Checked credentials: ${creds}` : '');
|
|
35
79
|
});
|
|
36
80
|
return textResult(`${humans.length} expert${humans.length === 1 ? '' : 's'}:\n${lines.join('\n')}\n\n` +
|
|
37
81
|
'Prices and live status are not listed here; use get_quote with a humanId to see them.');
|
|
@@ -97,6 +141,17 @@ function hours(minutes) {
|
|
|
97
141
|
const n = Number(minutes);
|
|
98
142
|
return n % 60 === 0 ? `${n / 60} hour${n === 60 ? '' : 's'}` : `${n} minutes`;
|
|
99
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
|
+
}
|
|
100
155
|
export function describeQuote(q) {
|
|
101
156
|
const human = q.human;
|
|
102
157
|
const who = human ? `${cleanIncoming(String(human.name || 'This person'), 60)} (humanId: ${human.humanId})` : 'This person';
|
|
@@ -106,11 +161,11 @@ export function describeQuote(q) {
|
|
|
106
161
|
}
|
|
107
162
|
return (`${who} is ${human?.live ? 'live now' : 'offline; a chat request waits up to 24 hours for them'}.\n` +
|
|
108
163
|
`Price: ${blockPrice(q.pricing, q.mode)}, charged in full once they accept (nothing if they never do). ` +
|
|
109
|
-
`${
|
|
164
|
+
`${firstCharge(q)}\n` +
|
|
110
165
|
`Length: one hour to start; extend_session adds another hour, up to ${hours(q.maxSessionMinutes)} ` +
|
|
111
166
|
`within your ${dollars(q.spendLimitCents)} spend limit. Ending early does not refund paid time.\n` +
|
|
112
167
|
`All sales are final.` +
|
|
113
|
-
(q.paymentReady === false ? `\
|
|
168
|
+
(q.paymentReady === false ? `\n${NO_PAYMENT_HINT}` : ''));
|
|
114
169
|
}
|
|
115
170
|
export async function handleGetQuote(args, ctx) {
|
|
116
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
|
}
|
|
@@ -169,7 +169,7 @@ export async function handleCheckConnection(args, ctx) {
|
|
|
169
169
|
'Nothing is charged unless they accept; call end_session to cancel and release the hold.');
|
|
170
170
|
}
|
|
171
171
|
if (status === 'ENDED')
|
|
172
|
-
return textResult(`The session has ended. ${chargedText(res.data.amountCents)}`);
|
|
172
|
+
return textResult(`The session has ended. ${chargedText(res.data.amountCents)}${rateHint(res.data.amountCents)}`);
|
|
173
173
|
if (status === 'ACCEPTED' || status === 'ACTIVE') {
|
|
174
174
|
if (!ctx.live)
|
|
175
175
|
return errorResult('Live chat is not available on the hosted MCP server.');
|
|
@@ -298,6 +298,21 @@ 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
|
+
: '';
|
|
314
|
+
// After a session the person took (it was charged), invite a thumbs up/down.
|
|
315
|
+
const rateHint = (amountCents) => Number(amountCents) > 0 ? '\nDid they help? Give a thumbs up or down with rate_human and this session_id.' : '';
|
|
301
316
|
export async function endSessionViaApi(ctx, sessionId) {
|
|
302
317
|
const res = await callApi(ctx, `/api/connect/${sessionId}/end`, { method: 'POST', body: {} });
|
|
303
318
|
if (!res.ok && res.status !== 404 && res.status !== 409) {
|
|
@@ -314,8 +329,9 @@ export async function handleEndSession(args, ctx) {
|
|
|
314
329
|
else
|
|
315
330
|
await endSessionViaApi(ctx, sessionId);
|
|
316
331
|
const res = await callApi(ctx, `/api/connect/${sessionId}`);
|
|
317
|
-
if (res.ok && res.data.status === 'ENDED')
|
|
318
|
-
return textResult(`Session ended. ${chargedText(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
|
+
}
|
|
319
335
|
return textResult('Session ended. Use check_connection in a moment to see the final charge.');
|
|
320
336
|
}
|
|
321
337
|
catch (err) {
|
|
@@ -356,13 +372,13 @@ export async function handleExtendSession(args, ctx) {
|
|
|
356
372
|
const pricing = (data.pricing || {});
|
|
357
373
|
const blocks = Number(data.blocksPurchased ?? 1);
|
|
358
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` +
|
|
359
|
-
`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` +
|
|
360
376
|
`All sales are final.\n\nApprove this extension?`, 'the chat was not extended');
|
|
361
377
|
if (stop)
|
|
362
378
|
return stop;
|
|
363
379
|
const res = await callApi(ctx, `/api/connect/${sessionId}/extend`, { method: 'POST', body: {} });
|
|
364
380
|
if (res.status === 402) {
|
|
365
|
-
return errorResult(
|
|
381
|
+
return errorResult(`Payment required: the extra block could not be paid. ${paymentHint(res.data)} The chat was not extended.`);
|
|
366
382
|
}
|
|
367
383
|
if (res.status === 409) {
|
|
368
384
|
const why = typeof res.data.message === 'string' ? res.data.message : apiErrorText(res, 'Cannot extend');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { ToolContext, ToolResult } from '../context.js';
|
|
3
|
+
export declare const rateHumanTool: Tool;
|
|
4
|
+
export declare function handleRateHuman(args: Record<string, unknown> | undefined, ctx: ToolContext): Promise<ToolResult>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ApiError, apiErrorText, callApi, errorResult, textResult } from '../context.js';
|
|
2
|
+
export const rateHumanTool = {
|
|
3
|
+
name: 'rate_human',
|
|
4
|
+
description: 'After a live chat or video session ends, or after a free question is answered, give the person a thumbs up or ' +
|
|
5
|
+
'thumbs down (once). It feeds their public thumbs-up rate, which helps other agents choose. Pass session_id for a ' +
|
|
6
|
+
'live session or question_id for an answer.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
session_id: { type: 'string', description: 'The Session ID from connect_human.' },
|
|
11
|
+
question_id: { type: 'string', description: 'The questionId from ask_human.' },
|
|
12
|
+
rating: { type: 'string', enum: ['up', 'down'], description: 'up: they helped; down: they did not.' },
|
|
13
|
+
comment: { type: 'string', description: 'Optional: a short reason (up to 500 characters).', maxLength: 500 },
|
|
14
|
+
},
|
|
15
|
+
required: ['rating'],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
const ID = /^[A-Za-z0-9_:.-]{1,128}$/;
|
|
19
|
+
export async function handleRateHuman(args, ctx) {
|
|
20
|
+
const rating = args?.rating;
|
|
21
|
+
if (rating !== 'up' && rating !== 'down')
|
|
22
|
+
return errorResult('Error: rating must be "up" or "down".');
|
|
23
|
+
const sessionId = typeof args?.session_id === 'string' ? args.session_id.trim() : '';
|
|
24
|
+
const questionId = typeof args?.question_id === 'string' ? args.question_id.trim() : '';
|
|
25
|
+
if (Boolean(sessionId) === Boolean(questionId))
|
|
26
|
+
return errorResult('Error: pass exactly one of session_id or question_id.');
|
|
27
|
+
const id = sessionId || questionId;
|
|
28
|
+
if (!ID.test(id))
|
|
29
|
+
return errorResult('Error: that id is not valid.');
|
|
30
|
+
const comment = typeof args?.comment === 'string' && args.comment.trim() ? args.comment.trim().slice(0, 500) : undefined;
|
|
31
|
+
const path = sessionId ? `/api/connect/${id}/feedback` : `/api/questions/${id}/feedback`;
|
|
32
|
+
try {
|
|
33
|
+
const res = await callApi(ctx, path, { method: 'POST', body: { rating, ...(comment && { comment }) } });
|
|
34
|
+
if (res.status === 409)
|
|
35
|
+
return textResult('You already rated this one. Thanks!');
|
|
36
|
+
if (!res.ok)
|
|
37
|
+
return errorResult(apiErrorText(res, 'Failed to save the rating'));
|
|
38
|
+
return textResult(`Saved: thumbs ${rating}. Thanks, this helps other agents choose.`);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return errorResult(err instanceof ApiError ? err.message : 'Failed to save the rating.');
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
|
-
"description": "MCP server for askingahuman.com: let AI agents reach real people, with free questions and capped, paid live chats",
|
|
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",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"bin": {
|