@askingahuman/mcp-server 1.0.0 → 2.0.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 +64 -52
- package/dist/config.d.ts +10 -0
- package/dist/config.js +37 -0
- package/dist/context.d.ts +33 -0
- package/dist/context.js +42 -0
- package/dist/http-server.d.ts +1 -0
- package/dist/http-server.js +53 -0
- package/dist/index.js +31 -35
- package/dist/live/bridge.d.ts +68 -0
- package/dist/live/bridge.js +217 -0
- package/dist/safety.d.ts +16 -0
- package/dist/safety.js +70 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +70 -0
- package/dist/tools/ask-human.js +167 -38
- package/dist/tools/check-answer.js +99 -34
- package/dist/tools/discover.d.ts +18 -0
- package/dist/tools/discover.js +118 -0
- package/dist/tools/live.d.ts +18 -0
- package/dist/tools/live.js +380 -0
- package/dist/tools/reply-to-human.d.ts +14 -0
- package/dist/tools/reply-to-human.js +72 -0
- package/dist/tools/submit-feedback.d.ts +15 -0
- package/dist/tools/submit-feedback.js +77 -0
- package/dist/tools/who-is-live.d.ts +4 -0
- package/dist/tools/who-is-live.js +53 -0
- package/package.json +21 -8
package/README.md
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
# askingahuman MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Let your AI agent reach a real person through [askingahuman.com](https://askingahuman.com): send a free question to someone you choose, or book a paid live chat with them.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Every request names a person. Find one with `list_humans` or `who_is_live`, then pass their `humanId`.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Tools
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**Free**
|
|
10
|
+
- `list_humans`: browse expert profiles (name, title, specializations, rating).
|
|
11
|
+
- `who_is_live`: see who is live right now. It can wait up to 50 seconds for someone to come online.
|
|
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.
|
|
14
|
+
- `reply_to_human`: continue the conversation after an answer.
|
|
15
|
+
- `get_quote`: see what a live chat with a person would cost, what is held on the card, and how long it can run. Nothing is charged.
|
|
16
|
+
- `submit_feedback`: send feedback about the service.
|
|
10
17
|
|
|
11
|
-
**
|
|
12
|
-
- `
|
|
13
|
-
- `
|
|
18
|
+
**Paid live chat** (local server only, text only)
|
|
19
|
+
- `connect_human`: request a live chat with a person. Shows the cost to your user for approval when the client supports it. If the person is offline, the request waits up to 24 hours for them.
|
|
20
|
+
- `check_connection`: wait for the person to accept. You then join the chat automatically. Shows the paid time left.
|
|
21
|
+
- `read_messages` / `send_message`: talk in the chat. `read_messages` warns when less than 10 minutes of paid time remain.
|
|
22
|
+
- `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
|
+
|
|
25
|
+
### How live chat is priced
|
|
26
|
+
|
|
27
|
+
Each person sets their own price per hour of chat. The first hour is held on the card 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.
|
|
14
28
|
|
|
15
29
|
## Setup
|
|
16
30
|
|
|
17
|
-
### 1. Get your
|
|
31
|
+
### 1. Get your API key
|
|
18
32
|
|
|
19
|
-
|
|
33
|
+
Create a key at [askingahuman.com/developers](https://askingahuman.com/developers). To use live chat, add a card there too.
|
|
20
34
|
|
|
21
|
-
### 2. Add to your MCP client
|
|
35
|
+
### 2. Add it to your MCP client
|
|
22
36
|
|
|
23
|
-
**Claude Desktop** (`claude_desktop_config.json`):
|
|
37
|
+
**Claude Desktop** (`claude_desktop_config.json`) or **Cursor** (Settings > MCP Servers):
|
|
24
38
|
|
|
25
39
|
```json
|
|
26
40
|
{
|
|
@@ -29,68 +43,66 @@ Visit [askingahuman.com/developers](https://askingahuman.com/developers) and cre
|
|
|
29
43
|
"command": "npx",
|
|
30
44
|
"args": ["-y", "@askingahuman/mcp-server"],
|
|
31
45
|
"env": {
|
|
32
|
-
"ASKINGAHUMAN_API_KEY": "ahk_your_key_here"
|
|
46
|
+
"ASKINGAHUMAN_API_KEY": "ahk_your_key_here",
|
|
47
|
+
"ASKINGAHUMAN_MAX_SPEND_USD": "100"
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
52
|
```
|
|
38
53
|
|
|
39
|
-
|
|
54
|
+
Live chat uses the optional package `@livekit/rtc-node` (installed automatically on Windows, macOS and Linux). Without it, the free tools still work.
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"env": {
|
|
47
|
-
"ASKINGAHUMAN_API_KEY": "ahk_your_key_here"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
56
|
+
### Docker
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
docker build -t askingahuman/mcp-server .
|
|
60
|
+
docker run -i --rm -e ASKINGAHUMAN_API_KEY=ahk_your_key_here askingahuman/mcp-server
|
|
51
61
|
```
|
|
52
62
|
|
|
53
|
-
##
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
| Variable | Default | What it does |
|
|
66
|
+
|----------|---------|--------------|
|
|
67
|
+
| `ASKINGAHUMAN_API_KEY` | (required) | Your API key. |
|
|
68
|
+
| `ASKINGAHUMAN_MAX_SPEND_USD` | `100` | The most one live chat can cost, extensions included (max 200, the platform cap). The agent can ask for less, never more. |
|
|
69
|
+
| `ASKINGAHUMAN_REQUIRE_CONFIRMATION` | `false` | If `true`, paid chats and extensions happen only when your MCP client can ask you to approve the cost. |
|
|
70
|
+
| `ASKINGAHUMAN_SKIP_APPROVAL` | `false` | If `true`, the server doesn't ask for cost approval in the chat. Use it with clients that claim to show MCP approval forms but don't (for example, some editor extensions decline them automatically). Your client still asks before each tool call, and the spend limit still applies. Ignored when `ASKINGAHUMAN_REQUIRE_CONFIRMATION` is on. |
|
|
71
|
+
| `ASKINGAHUMAN_API_URL` | `https://api.askingahuman.com` | API address (https only). |
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|------|-------|---------|
|
|
57
|
-
| Free | $0 | 1 question/month, no credit card needed |
|
|
58
|
-
| Paid | $5/question | After free tier; requires saved payment method |
|
|
73
|
+
## Safety
|
|
59
74
|
|
|
60
|
-
|
|
75
|
+
- **Spending is capped twice.** Your spend limit is enforced in this server and again by the API, which won't sell an hour, or an extension, that would go past it.
|
|
76
|
+
- **You approve each charge.** `connect_human` and `extend_session` show the cost to your user first when the client supports it.
|
|
77
|
+
- **Nothing is left waiting.** When the MCP client shuts down, the server ends its open chats and cancels requests nobody has accepted yet, which releases their holds. The platform ends a chat when its paid time runs out.
|
|
78
|
+
- **Nothing sensitive goes out.** Messages to people are blocked if they contain your API key or anything that looks like a credential (cloud keys, tokens, private keys). Links are blocked too, so an agent can't send someone to a download or a phishing page.
|
|
79
|
+
- **Text only, no files.** The server joins the chat without receiving audio or video and ignores everything except plain-text chat from the person. Files can't be sent or received.
|
|
80
|
+
- **People's words are framed as untrusted.** Messages and answers from people are wrapped in `<human_message>` tags with a note telling the model to treat them as conversation, not instructions.
|
|
81
|
+
- **Private by default.** Chats are kept in memory only, never written to disk or logged, and dropped 10 minutes after they end. The chat's access token stays inside this server and is never shown to the model.
|
|
61
82
|
|
|
62
83
|
## Example
|
|
63
84
|
|
|
64
|
-
|
|
85
|
+
1. `list_humans` or `who_is_live` finds the right person.
|
|
86
|
+
2. `get_quote` with their `humanId` shows the price per hour and the hold.
|
|
87
|
+
3. `connect_human` asks your user to approve, then notifies the person.
|
|
88
|
+
4. `check_connection` with `wait_seconds: 45` waits until they accept, then joins the chat.
|
|
89
|
+
5. `send_message` / `read_messages` until done. `extend_session` if you need more time; `end_session` to leave.
|
|
65
90
|
|
|
66
|
-
|
|
67
|
-
2. Receive a `questionId` back immediately
|
|
68
|
-
3. Periodically call `check_answer` with that ID
|
|
69
|
-
4. Eventually receive a thoughtful, human-written response
|
|
91
|
+
Just need an answer? `ask_human` with their `humanId`, then `check_answer` later.
|
|
70
92
|
|
|
71
93
|
## REST API
|
|
72
94
|
|
|
73
|
-
You can also use the API directly without the MCP server:
|
|
74
|
-
|
|
75
95
|
```bash
|
|
76
|
-
#
|
|
77
|
-
curl -X POST https://askingahuman.com/api/questions \
|
|
78
|
-
-H "Authorization: Bearer ahk_your_key" \
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
curl https://askingahuman.com/api/
|
|
84
|
-
-H "Authorization: Bearer ahk_your_key"
|
|
96
|
+
# Ask a chosen person a question (free)
|
|
97
|
+
curl -X POST https://api.askingahuman.com/api/questions \
|
|
98
|
+
-H "Authorization: Bearer ahk_your_key" -H "Content-Type: application/json" \
|
|
99
|
+
-d '{"questionText": "Your question here", "targetExpertId": "HUMAN_ID"}'
|
|
100
|
+
|
|
101
|
+
# Who is live, and what would an hour of chat with them cost?
|
|
102
|
+
curl https://api.askingahuman.com/api/live-now -H "Authorization: Bearer ahk_your_key"
|
|
103
|
+
curl "https://api.askingahuman.com/api/connect/quote?mode=message&humanId=HUMAN_ID" -H "Authorization: Bearer ahk_your_key"
|
|
85
104
|
```
|
|
86
105
|
|
|
87
|
-
## Environment Variables
|
|
88
|
-
|
|
89
|
-
| Variable | Required | Description |
|
|
90
|
-
|----------|----------|-------------|
|
|
91
|
-
| `ASKINGAHUMAN_API_KEY` | Yes | Your API key from askingahuman.com/developers |
|
|
92
|
-
| `ASKINGAHUMAN_API_URL` | No | Custom API URL (defaults to production) |
|
|
93
|
-
|
|
94
106
|
## License
|
|
95
107
|
|
|
96
108
|
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const DEFAULT_API_BASE = "https://api.askingahuman.com";
|
|
2
|
+
export declare const PLATFORM_MAX_SPEND_CENTS = 20000;
|
|
3
|
+
export interface McpConfig {
|
|
4
|
+
apiBase: string;
|
|
5
|
+
maxSpendCents: number;
|
|
6
|
+
requireConfirmation: boolean;
|
|
7
|
+
skipApproval: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function resolveApiBase(raw: string | undefined): string;
|
|
10
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): McpConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Settings the person running this MCP server controls through environment
|
|
2
|
+
// variables. The spend limit set here binds the agent: tool arguments can only
|
|
3
|
+
// lower it, and the platform caps every session at $200 (and chat at 4 hours)
|
|
4
|
+
// regardless.
|
|
5
|
+
export const DEFAULT_API_BASE = 'https://api.askingahuman.com';
|
|
6
|
+
export const PLATFORM_MAX_SPEND_CENTS = 20000;
|
|
7
|
+
function boundedNumber(raw, fallback, min, max) {
|
|
8
|
+
const parsed = Number(raw);
|
|
9
|
+
if (raw === undefined || raw.trim() === '' || !Number.isFinite(parsed))
|
|
10
|
+
return fallback;
|
|
11
|
+
return Math.min(max, Math.max(min, parsed));
|
|
12
|
+
}
|
|
13
|
+
// The API key travels in every request, so only HTTPS is accepted (plain HTTP
|
|
14
|
+
// only for a local development API).
|
|
15
|
+
export function resolveApiBase(raw) {
|
|
16
|
+
const value = (raw || DEFAULT_API_BASE).trim().replace(/\/+$/, '');
|
|
17
|
+
let url;
|
|
18
|
+
try {
|
|
19
|
+
url = new URL(value);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error(`ASKINGAHUMAN_API_URL is not a valid URL: ${value}`);
|
|
23
|
+
}
|
|
24
|
+
const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
|
|
25
|
+
if (url.protocol !== 'https:' && !(local && url.protocol === 'http:')) {
|
|
26
|
+
throw new Error('ASKINGAHUMAN_API_URL must use https:// (http:// is allowed only for localhost).');
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export function loadConfig(env = process.env) {
|
|
31
|
+
return {
|
|
32
|
+
apiBase: resolveApiBase(env.ASKINGAHUMAN_API_URL),
|
|
33
|
+
maxSpendCents: Math.round(boundedNumber(env.ASKINGAHUMAN_MAX_SPEND_USD, 100, 0.01, PLATFORM_MAX_SPEND_CENTS / 100) * 100),
|
|
34
|
+
requireConfirmation: /^(1|true|yes)$/i.test(env.ASKINGAHUMAN_REQUIRE_CONFIRMATION || ''),
|
|
35
|
+
skipApproval: /^(1|true|yes)$/i.test(env.ASKINGAHUMAN_SKIP_APPROVAL || ''),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { McpConfig } from './config.js';
|
|
2
|
+
import type { LiveBridge } from './live/bridge.js';
|
|
3
|
+
export type Confirmation = 'accepted' | 'declined' | 'cancelled' | 'unsupported';
|
|
4
|
+
export interface ToolContext {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
config: McpConfig;
|
|
7
|
+
live: LiveBridge | null;
|
|
8
|
+
confirm: (message: string) => Promise<Confirmation>;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolResult {
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
content: Array<{
|
|
13
|
+
type: 'text';
|
|
14
|
+
text: string;
|
|
15
|
+
}>;
|
|
16
|
+
isError?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function textResult(text: string): ToolResult;
|
|
19
|
+
export declare function errorResult(text: string): ToolResult;
|
|
20
|
+
export interface ApiResponse {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
status: number;
|
|
23
|
+
data: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export declare class ApiError extends Error {
|
|
26
|
+
}
|
|
27
|
+
export declare function callApi(ctx: Pick<ToolContext, 'apiKey' | 'config'>, path: string, init?: {
|
|
28
|
+
method?: string;
|
|
29
|
+
body?: unknown;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
}): Promise<ApiResponse>;
|
|
32
|
+
export declare function apiErrorText(res: ApiResponse, fallback: string): string;
|
|
33
|
+
export declare function dollars(cents: unknown): string;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function textResult(text) {
|
|
2
|
+
return { content: [{ type: 'text', text }] };
|
|
3
|
+
}
|
|
4
|
+
export function errorResult(text) {
|
|
5
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
6
|
+
}
|
|
7
|
+
export class ApiError extends Error {
|
|
8
|
+
}
|
|
9
|
+
// JSON request to the askingahuman API with the caller's key and a timeout.
|
|
10
|
+
// Throws ApiError with a readable message on network failure or timeout.
|
|
11
|
+
export async function callApi(ctx, path, init = {}) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timeoutId = setTimeout(() => controller.abort(), init.timeoutMs ?? 30_000);
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(`${ctx.config.apiBase}${path}`, {
|
|
16
|
+
method: init.method ?? 'GET',
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
19
|
+
...(init.body !== undefined && { 'Content-Type': 'application/json' }),
|
|
20
|
+
},
|
|
21
|
+
...(init.body !== undefined && { body: JSON.stringify(init.body) }),
|
|
22
|
+
signal: controller.signal,
|
|
23
|
+
});
|
|
24
|
+
const data = (await response.json().catch(() => ({})));
|
|
25
|
+
return { ok: response.ok, status: response.status, data };
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
29
|
+
throw new ApiError('The askingahuman API did not respond within 30 seconds. Try again shortly.');
|
|
30
|
+
}
|
|
31
|
+
throw new ApiError(`Network error: ${err instanceof Error ? err.message : 'unknown error'}.`);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
clearTimeout(timeoutId);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function apiErrorText(res, fallback) {
|
|
38
|
+
return `Error (${res.status}): ${typeof res.data.error === 'string' ? res.data.error : fallback}`;
|
|
39
|
+
}
|
|
40
|
+
export function dollars(cents) {
|
|
41
|
+
return `$${(Number(cents) / 100).toFixed(2)}`;
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { loadConfig } from './config.js';
|
|
4
|
+
import { createMcpServer } from './server.js';
|
|
5
|
+
// Hosted server: stateless per request, so no live chat (that needs a
|
|
6
|
+
// long-lived connection); free questions, discovery and quotes only.
|
|
7
|
+
const config = loadConfig();
|
|
8
|
+
const PORT = parseInt(process.env.PORT || '3000', 10);
|
|
9
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
10
|
+
// Health check
|
|
11
|
+
if (req.url === '/health' && req.method === 'GET') {
|
|
12
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
13
|
+
res.end(JSON.stringify({ status: 'ok', service: 'askingahuman-mcp' }));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
// MCP endpoint
|
|
17
|
+
if (req.url === '/mcp' && req.method === 'POST') {
|
|
18
|
+
// Extract API key from Authorization header
|
|
19
|
+
const authHeader = req.headers.authorization;
|
|
20
|
+
if (!authHeader || !authHeader.startsWith('Bearer ahk_')) {
|
|
21
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
22
|
+
res.end(JSON.stringify({ error: 'API key required. Format: Bearer ahk_...' }));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const apiKey = authHeader.slice(7);
|
|
26
|
+
try {
|
|
27
|
+
const server = createMcpServer({ apiKey, config, live: null });
|
|
28
|
+
const transport = new StreamableHTTPServerTransport({
|
|
29
|
+
sessionIdGenerator: undefined, // Stateless
|
|
30
|
+
});
|
|
31
|
+
await server.connect(transport);
|
|
32
|
+
await transport.handleRequest(req, res);
|
|
33
|
+
// Clean up after response
|
|
34
|
+
await server.close();
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.error('MCP request error:', err);
|
|
38
|
+
if (!res.headersSent) {
|
|
39
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
40
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// 404 for everything else
|
|
46
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
47
|
+
res.end(JSON.stringify({ error: 'Not found. Use POST /mcp for MCP requests, GET /health for health check.' }));
|
|
48
|
+
});
|
|
49
|
+
httpServer.listen(PORT, () => {
|
|
50
|
+
console.log(`askingahuman MCP HTTP server listening on port ${PORT}`);
|
|
51
|
+
console.log(`Health: http://localhost:${PORT}/health`);
|
|
52
|
+
console.log(`MCP: POST http://localhost:${PORT}/mcp`);
|
|
53
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,39 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const server = new Server({ name: 'askingahuman', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
9
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
10
|
-
tools: [askHumanTool, checkAnswerTool]
|
|
11
|
-
}));
|
|
12
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
13
|
-
const apiKey = process.env.ASKINGAHUMAN_API_KEY;
|
|
14
|
-
if (!apiKey) {
|
|
15
|
-
return {
|
|
16
|
-
content: [{
|
|
17
|
-
type: 'text',
|
|
18
|
-
text: 'Error: ASKINGAHUMAN_API_KEY environment variable is required. Get your free key at https://askingahuman.com/developers'
|
|
19
|
-
}],
|
|
20
|
-
isError: true
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
switch (request.params.name) {
|
|
24
|
-
case 'ask_human':
|
|
25
|
-
return handleAskHuman(request.params.arguments, apiKey, API_BASE);
|
|
26
|
-
case 'check_answer':
|
|
27
|
-
return handleCheckAnswer(request.params.arguments, apiKey, API_BASE);
|
|
28
|
-
default:
|
|
29
|
-
return {
|
|
30
|
-
content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
|
|
31
|
-
isError: true
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
});
|
|
3
|
+
import { loadConfig } from './config.js';
|
|
4
|
+
import { LiveBridge, loadLiveKit } from './live/bridge.js';
|
|
5
|
+
import { createMcpServer } from './server.js';
|
|
6
|
+
import { endSessionViaApi } from './tools/live.js';
|
|
35
7
|
async function main() {
|
|
36
|
-
const
|
|
37
|
-
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
const apiKey = process.env.ASKINGAHUMAN_API_KEY;
|
|
10
|
+
// Local (stdio) server: this process can hold live chats open for the agent.
|
|
11
|
+
const live = new LiveBridge({
|
|
12
|
+
loadRoomFactory: loadLiveKit,
|
|
13
|
+
endSession: (sessionId) => endSessionViaApi({ apiKey: apiKey ?? '', config }, sessionId),
|
|
14
|
+
});
|
|
15
|
+
const server = createMcpServer({ apiKey, config, live });
|
|
16
|
+
// When the client goes away: end open chats and cancel requests nobody has
|
|
17
|
+
// accepted yet (that releases their holds).
|
|
18
|
+
// On a signal, exit after that; when stdin closes, let in-flight requests
|
|
19
|
+
// finish and the process exit on its own (all bridge timers are unref'd).
|
|
20
|
+
let stopping = null;
|
|
21
|
+
const endChats = () => (stopping ??= live.shutdown().catch(() => undefined));
|
|
22
|
+
const exitAfterEnding = async () => {
|
|
23
|
+
await Promise.race([endChats(), new Promise((resolve) => setTimeout(resolve, 5000))]);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
};
|
|
26
|
+
process.on('SIGINT', exitAfterEnding);
|
|
27
|
+
process.on('SIGTERM', exitAfterEnding);
|
|
28
|
+
process.stdin.on('end', endChats);
|
|
29
|
+
process.stdin.on('close', endChats);
|
|
30
|
+
await server.connect(new StdioServerTransport());
|
|
38
31
|
}
|
|
39
|
-
main().catch(
|
|
32
|
+
main().catch((err) => {
|
|
33
|
+
console.error(err instanceof Error ? err.message : err);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface ChatRoom {
|
|
2
|
+
on(event: 'dataReceived', cb: (payload: Uint8Array, participant?: {
|
|
3
|
+
identity: string;
|
|
4
|
+
}, kind?: unknown, topic?: string) => void): unknown;
|
|
5
|
+
on(event: 'participantConnected' | 'participantDisconnected', cb: (participant: {
|
|
6
|
+
identity: string;
|
|
7
|
+
}) => void): unknown;
|
|
8
|
+
on(event: 'disconnected', cb: (reason?: unknown) => void): unknown;
|
|
9
|
+
connect(url: string, token: string, opts: {
|
|
10
|
+
autoSubscribe: boolean;
|
|
11
|
+
dynacast: boolean;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
disconnect(): Promise<void>;
|
|
14
|
+
localParticipant?: {
|
|
15
|
+
publishData(data: Uint8Array, opts: {
|
|
16
|
+
reliable?: boolean;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
remoteParticipants?: Map<string, {
|
|
20
|
+
identity: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export type RoomFactory = () => ChatRoom;
|
|
24
|
+
export declare function loadLiveKit(): Promise<RoomFactory | null>;
|
|
25
|
+
export interface ChatLine {
|
|
26
|
+
from: 'human' | 'you' | 'system';
|
|
27
|
+
text: string;
|
|
28
|
+
at: string;
|
|
29
|
+
}
|
|
30
|
+
export declare class LiveChatError extends Error {
|
|
31
|
+
}
|
|
32
|
+
export declare const MAX_ACTIVE_CHATS = 2;
|
|
33
|
+
export declare const MAX_MESSAGE_BYTES: number;
|
|
34
|
+
export declare const MAX_SENT_PER_SESSION = 200;
|
|
35
|
+
export declare const MIN_SEND_INTERVAL_MS = 1000;
|
|
36
|
+
export declare const FORGET_AFTER_END_MS: number;
|
|
37
|
+
export interface LiveBridgeOptions {
|
|
38
|
+
loadRoomFactory: () => Promise<RoomFactory | null>;
|
|
39
|
+
endSession: (sessionId: string) => Promise<void>;
|
|
40
|
+
now?: () => number;
|
|
41
|
+
}
|
|
42
|
+
export declare class LiveBridge {
|
|
43
|
+
private opts;
|
|
44
|
+
private chats;
|
|
45
|
+
private pending;
|
|
46
|
+
private factory;
|
|
47
|
+
private now;
|
|
48
|
+
constructor(opts: LiveBridgeOptions);
|
|
49
|
+
isAvailable(): Promise<boolean>;
|
|
50
|
+
private getFactory;
|
|
51
|
+
trackRequest(sessionId: string): void;
|
|
52
|
+
isJoined(sessionId: string): boolean;
|
|
53
|
+
join(sessionId: string, url: string, token: string): Promise<'joined' | 'already'>;
|
|
54
|
+
private onData;
|
|
55
|
+
private push;
|
|
56
|
+
private getChat;
|
|
57
|
+
send(sessionId: string, text: string): Promise<void>;
|
|
58
|
+
read(sessionId: string, waitMs: number): Promise<{
|
|
59
|
+
lines: ChatLine[];
|
|
60
|
+
closed: boolean;
|
|
61
|
+
closeReason: string | null;
|
|
62
|
+
humanPresent: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
transcript(sessionId: string): ChatLine[];
|
|
65
|
+
end(sessionId: string, reason?: string): Promise<boolean>;
|
|
66
|
+
private close;
|
|
67
|
+
shutdown(): Promise<void>;
|
|
68
|
+
}
|