@pyai/mcp 0.4.0 → 0.5.1
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/AGENT_GUIDE.md +26 -0
- package/README.md +26 -0
- package/dist/extra-tools.js +5 -4
- package/dist/generated-tools.js +26 -4
- package/dist/hosted-policy.js +33 -0
- package/dist/server.js +11 -9
- package/dist/tools.js +59 -13
- package/dist/workflows.js +19 -0
- package/package.json +1 -1
package/AGENT_GUIDE.md
CHANGED
|
@@ -72,3 +72,29 @@ the corresponding session. Read it with `omni_get_call`, `omni_get_summary`,
|
|
|
72
72
|
`omni_get_transcript`, or `omni_get_recording` with `response: "url"`. Recording
|
|
73
73
|
URLs require REST authentication and are not public download links. For inbound
|
|
74
74
|
calls use `omni_list_calls`, filtering by `session_label`.
|
|
75
|
+
|
|
76
|
+
## Discovery and identity output
|
|
77
|
+
|
|
78
|
+
In MCP 0.5.0, use `discover_tools({"query":"voice agent"})`; `task` is an
|
|
79
|
+
alias. If both are present, they must match after trimming. Omit both to list
|
|
80
|
+
workflows. Empty or conflicting values return `invalid_arguments`.
|
|
81
|
+
|
|
82
|
+
`whoami({})` returns concise text showing environment, scopes and account
|
|
83
|
+
limits. Authorized organization, project and key IDs remain in structured
|
|
84
|
+
output; `whoami({"verbose":true})` also includes them in text. This reduces
|
|
85
|
+
prompt clutter, not host access to identity metadata. Credential-shaped fields
|
|
86
|
+
are omitted in both modes. Never paste keys or full identity dumps into reports.
|
|
87
|
+
|
|
88
|
+
## Hosted connection boundaries
|
|
89
|
+
|
|
90
|
+
The hosted OAuth MCP connection uses existing PyAI projects and owned phone numbers.
|
|
91
|
+
Number purchasing is not exposed through hosted MCP. The REST API and local stdio
|
|
92
|
+
integration retain their separate contracts. Hosted agent tool bindings accept
|
|
93
|
+
saved tool IDs and enabled flags, not credentials or arbitrary connection settings.
|
|
94
|
+
Configure those settings privately in the PyAI console. Retained bindings preserve
|
|
95
|
+
their saved settings, and hosted agent responses omit connection configuration.
|
|
96
|
+
|
|
97
|
+
Supply only audio and text you are authorized to process. Do not submit payment-card
|
|
98
|
+
data, protected health information, government identifiers, passwords, or API keys.
|
|
99
|
+
If an operation exceeds the project's entitlement, report the limitation without
|
|
100
|
+
initiating a purchase or repeatedly resubmitting the request.
|
package/README.md
CHANGED
|
@@ -126,3 +126,29 @@ Calling reaches real phone destinations and is not a sandbox simulation.
|
|
|
126
126
|
Use the SDK's `pyai.omni.connect({ rate, configure })` for realtime audio. The
|
|
127
127
|
[Omni frame contract](https://api.pyai.com/omni-frames.json) is generated from
|
|
128
128
|
the existing AsyncAPI protocol source.
|
|
129
|
+
|
|
130
|
+
## Discovery and identity output
|
|
131
|
+
|
|
132
|
+
In MCP 0.5.0, use `discover_tools({"query":"voice agent"})`; `task` is an
|
|
133
|
+
alias. If both are present, they must match after trimming. Omit both to list
|
|
134
|
+
workflows. Empty or conflicting values return `invalid_arguments`.
|
|
135
|
+
|
|
136
|
+
`whoami({})` returns concise text showing environment, scopes and account
|
|
137
|
+
limits. Authorized organization, project and key IDs remain in structured
|
|
138
|
+
output; `whoami({"verbose":true})` also includes them in text. This reduces
|
|
139
|
+
prompt clutter, not host access to identity metadata. Credential-shaped fields
|
|
140
|
+
are omitted in both modes. Never paste keys or full identity dumps into reports.
|
|
141
|
+
|
|
142
|
+
## Hosted connection boundaries
|
|
143
|
+
|
|
144
|
+
The hosted OAuth MCP connection uses existing PyAI projects and owned phone numbers.
|
|
145
|
+
Number purchasing is not exposed through hosted MCP. The REST API and local stdio
|
|
146
|
+
integration retain their separate contracts. Hosted agent tool bindings accept
|
|
147
|
+
saved tool IDs and enabled flags, not credentials or arbitrary connection settings.
|
|
148
|
+
Configure those settings privately in the PyAI console. Retained bindings preserve
|
|
149
|
+
their saved settings, and hosted agent responses omit connection configuration.
|
|
150
|
+
|
|
151
|
+
Supply only audio and text you are authorized to process. Do not submit payment-card
|
|
152
|
+
data, protected health information, government identifiers, passwords, or API keys.
|
|
153
|
+
If an operation exceeds the project's entitlement, report the limitation without
|
|
154
|
+
initiating a purchase or repeatedly resubmitting the request.
|
package/dist/extra-tools.js
CHANGED
|
@@ -18,6 +18,7 @@ export const EXTRA_TOOLS = [
|
|
|
18
18
|
description: "Find the right PyAI tools and a short recipe for a use case: speech, STT, voice agents, compliance, summaries, dubbing or narration. No credentials or usage.",
|
|
19
19
|
inputSchema: obj({
|
|
20
20
|
query: string("Product name or task, e.g. “translate audio”, “voice agent”, “TTS”."),
|
|
21
|
+
task: string("Alias for query. If both are supplied, they must match after trimming."),
|
|
21
22
|
}),
|
|
22
23
|
},
|
|
23
24
|
{
|
|
@@ -27,7 +28,7 @@ export const EXTRA_TOOLS = [
|
|
|
27
28
|
},
|
|
28
29
|
{
|
|
29
30
|
name: "create_dub",
|
|
30
|
-
description: "
|
|
31
|
+
description: "Fetch audio from the user-authorized HTTPS source URL and start a translation and dubbing job. Accesses an external source and consumes usage. Keep job_id and poll get_dub_job; do not submit again after a timeout.",
|
|
31
32
|
inputSchema: obj({
|
|
32
33
|
source_url: { type: "string", format: "uri", pattern: "^https://" },
|
|
33
34
|
source_lang: string("Source language from get_dub_capabilities."),
|
|
@@ -36,8 +37,8 @@ export const EXTRA_TOOLS = [
|
|
|
36
37
|
},
|
|
37
38
|
{
|
|
38
39
|
name: "get_dub_job",
|
|
39
|
-
title: "
|
|
40
|
-
description: "
|
|
40
|
+
title: "Get dubbing job status",
|
|
41
|
+
description: "Retrieve the status and available results of an existing dubbing job by job_id. This read does not start or repeat a job. Poll pending/processing states without resubmitting.",
|
|
41
42
|
inputSchema: obj(id, ["job_id"]),
|
|
42
43
|
},
|
|
43
44
|
{
|
|
@@ -87,7 +88,7 @@ export const EXTRA_TOOLS = [
|
|
|
87
88
|
},
|
|
88
89
|
language: {
|
|
89
90
|
type: "string",
|
|
90
|
-
enum: ["en", "es", "fr", "de", "hi", "it", "pt", "nl"],
|
|
91
|
+
enum: ["en", "es", "fr", "de", "hi", "it", "pt", "nl", "zh", "yue", "ar", "id", "ko", "th", "vi", "ja", "tr", "ms", "sv", "da", "fi", "pl", "cs", "fil", "fa", "el", "hu", "mk", "ro"],
|
|
91
92
|
},
|
|
92
93
|
}, ["audio_base64", "filename"]),
|
|
93
94
|
},
|
package/dist/generated-tools.js
CHANGED
|
@@ -609,7 +609,7 @@ export const API_TOOLS = [
|
|
|
609
609
|
"null"
|
|
610
610
|
],
|
|
611
611
|
"maxLength": 200,
|
|
612
|
-
"description": "
|
|
612
|
+
"description": "Stored delivery direction for instruct-capable voice tiers. Current Omni routes support neutral delivery only: omit or set null for neutral speech. A saved custom value is rejected when configuring Omni; an inline empty `configure.voice_instruct` resets it to neutral for that session."
|
|
613
613
|
},
|
|
614
614
|
"brain_model": {
|
|
615
615
|
"type": [
|
|
@@ -686,9 +686,20 @@ export const API_TOOLS = [
|
|
|
686
686
|
"es",
|
|
687
687
|
"de",
|
|
688
688
|
"hi",
|
|
689
|
+
"zh",
|
|
690
|
+
"it",
|
|
691
|
+
"pt",
|
|
692
|
+
"ja",
|
|
693
|
+
"ko",
|
|
689
694
|
null
|
|
690
695
|
],
|
|
691
|
-
"
|
|
696
|
+
"x-staged-language-ids": [
|
|
697
|
+
"ht",
|
|
698
|
+
"zh-CN",
|
|
699
|
+
"ar-MSA",
|
|
700
|
+
"ar-Gulf"
|
|
701
|
+
],
|
|
702
|
+
"description": "Requested language for this Agent's Omni sessions. `null` or absent means `en`. Availability is staged: public serving includes `en`, `fr`, `es`, `hi`, `it`, `pt`, `ja`, and `ko`; `de` and the legacy `zh` alias fall back to English unless explicitly enabled with qualified endpoints. Canonical staged route identifiers are exposed in `x-staged-language-ids` for internal integration only and are rejected by the public control plane until qualified. Inspect the session's `configured.language_active` and `language_fallback` fields before assuming the requested language is active. See the Language support reference."
|
|
692
703
|
},
|
|
693
704
|
"metadata": {
|
|
694
705
|
"type": [
|
|
@@ -936,7 +947,7 @@ export const API_TOOLS = [
|
|
|
936
947
|
"null"
|
|
937
948
|
],
|
|
938
949
|
"maxLength": 200,
|
|
939
|
-
"description": "
|
|
950
|
+
"description": "Stored delivery direction for instruct-capable voice tiers. Current Omni routes support neutral delivery only: omit or set null for neutral speech. A saved custom value is rejected when configuring Omni; an inline empty `configure.voice_instruct` resets it to neutral for that session."
|
|
940
951
|
},
|
|
941
952
|
"brain_model": {
|
|
942
953
|
"type": [
|
|
@@ -1013,9 +1024,20 @@ export const API_TOOLS = [
|
|
|
1013
1024
|
"es",
|
|
1014
1025
|
"de",
|
|
1015
1026
|
"hi",
|
|
1027
|
+
"zh",
|
|
1028
|
+
"it",
|
|
1029
|
+
"pt",
|
|
1030
|
+
"ja",
|
|
1031
|
+
"ko",
|
|
1016
1032
|
null
|
|
1017
1033
|
],
|
|
1018
|
-
"
|
|
1034
|
+
"x-staged-language-ids": [
|
|
1035
|
+
"ht",
|
|
1036
|
+
"zh-CN",
|
|
1037
|
+
"ar-MSA",
|
|
1038
|
+
"ar-Gulf"
|
|
1039
|
+
],
|
|
1040
|
+
"description": "Requested language for this Agent's Omni sessions. `null` or absent means `en`. Availability is staged: public serving includes `en`, `fr`, `es`, `hi`, `it`, `pt`, `ja`, and `ko`; `de` and the legacy `zh` alias fall back to English unless explicitly enabled with qualified endpoints. Canonical staged route identifiers are exposed in `x-staged-language-ids` for internal integration only and are rejected by the public control plane until qualified. Inspect the session's `configured.language_active` and `language_fallback` fields before assuming the requested language is active. See the Language support reference."
|
|
1019
1041
|
},
|
|
1020
1042
|
"metadata": {
|
|
1021
1043
|
"type": [
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Remote conversational clients use saved connections, never credential inputs
|
|
2
|
+
// or digital-service checkout. The REST API and local stdio retain their contract.
|
|
3
|
+
export const HOSTED_EXCLUDED_TOOLS = new Set(["create_sandbox_key", "telephony_buy_number"]);
|
|
4
|
+
export const AGENT_WRITE_TOOLS = new Set(["create_agent", "update_agent", "set_agent_tools"]);
|
|
5
|
+
export function hostedTools(tools) {
|
|
6
|
+
return tools.filter(t => !HOSTED_EXCLUDED_TOOLS.has(t.name)).map(t => {
|
|
7
|
+
if (!AGENT_WRITE_TOOLS.has(t.name))
|
|
8
|
+
return t;
|
|
9
|
+
const schema = structuredClone(t.inputSchema);
|
|
10
|
+
const body = schema.properties.body;
|
|
11
|
+
body.additionalProperties = false;
|
|
12
|
+
const bindings = t.name === "set_agent_tools" ? body : body.properties.tools;
|
|
13
|
+
if (bindings?.items) {
|
|
14
|
+
bindings.items = {
|
|
15
|
+
type: "object", required: ["tool_id"], additionalProperties: false,
|
|
16
|
+
properties: {
|
|
17
|
+
tool_id: { type: "string", description: "ID of an existing tool. Configure its connection privately in the PyAI console." },
|
|
18
|
+
enabled: { type: "boolean", default: true },
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
bindings.description = "Replace bindings using saved tool IDs. Existing settings are preserved for retained bindings. Credentials and connection settings are not accepted through MCP.";
|
|
22
|
+
}
|
|
23
|
+
return { ...t, inputSchema: schema, description: t.description + " Use saved tool IDs; configure connection settings privately in the PyAI console. Never supply credentials." };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function withoutBindingConfig(value) {
|
|
27
|
+
if (Array.isArray(value))
|
|
28
|
+
return value.map(withoutBindingConfig);
|
|
29
|
+
if (!value || typeof value !== "object")
|
|
30
|
+
return value;
|
|
31
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => key !== "config")
|
|
32
|
+
.map(([key, entry]) => [key, withoutBindingConfig(entry)]));
|
|
33
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, McpError, ErrorCode, } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
-
import { TOOLS, TOOL_SCOPES, callTool, localContext, } from "./tools.js";
|
|
4
|
-
import {
|
|
5
|
-
export const MCP_VERSION = "0.
|
|
3
|
+
import { TOOLS, HOSTED_TOOLS, TOOL_SCOPES, callTool, localContext, } from "./tools.js";
|
|
4
|
+
import { RESOURCES, workflowsForMode, guideForMode, instructionsForMode, } from "./workflows.js";
|
|
5
|
+
export const MCP_VERSION = "0.5.1";
|
|
6
6
|
export function createMcpServer(client, context = localContext) {
|
|
7
|
+
const tools = context.mode === "http" ? HOSTED_TOOLS : TOOLS;
|
|
8
|
+
const workflows = workflowsForMode(context.mode);
|
|
7
9
|
const server = new Server({
|
|
8
10
|
name: "pyai",
|
|
9
11
|
version: MCP_VERSION,
|
|
@@ -11,10 +13,10 @@ export function createMcpServer(client, context = localContext) {
|
|
|
11
13
|
websiteUrl: "https://pyai.com/mcp",
|
|
12
14
|
}, {
|
|
13
15
|
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
14
|
-
instructions:
|
|
16
|
+
instructions: instructionsForMode(context.mode),
|
|
15
17
|
});
|
|
16
18
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
17
|
-
tools:
|
|
19
|
+
tools: tools.map((t) => context.mode === "stdio"
|
|
18
20
|
? t
|
|
19
21
|
: {
|
|
20
22
|
...t,
|
|
@@ -36,7 +38,7 @@ export function createMcpServer(client, context = localContext) {
|
|
|
36
38
|
}),
|
|
37
39
|
}));
|
|
38
40
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
39
|
-
if (!
|
|
41
|
+
if (!tools.some((t) => t.name === request.params.name) ||
|
|
40
42
|
(context.mode === "http" && request.params.name === "create_sandbox_key"))
|
|
41
43
|
throw new McpError(ErrorCode.InvalidParams, "Unknown tool.");
|
|
42
44
|
return (await callTool(request.params.name, request.params.arguments ?? {}, client, context));
|
|
@@ -53,13 +55,13 @@ export function createMcpServer(client, context = localContext) {
|
|
|
53
55
|
{
|
|
54
56
|
uri: resource.uri,
|
|
55
57
|
mimeType: resource.mimeType,
|
|
56
|
-
text: resource.uri === "pyai://guide" ?
|
|
58
|
+
text: resource.uri === "pyai://guide" ? guideForMode(context.mode) : JSON.stringify(workflows),
|
|
57
59
|
},
|
|
58
60
|
],
|
|
59
61
|
};
|
|
60
62
|
});
|
|
61
63
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
62
|
-
prompts: Object.entries(
|
|
64
|
+
prompts: Object.entries(workflows).map(([name, w]) => ({
|
|
63
65
|
name,
|
|
64
66
|
title: w.title,
|
|
65
67
|
description: w.example,
|
|
@@ -73,7 +75,7 @@ export function createMcpServer(client, context = localContext) {
|
|
|
73
75
|
})),
|
|
74
76
|
}));
|
|
75
77
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
76
|
-
const workflow =
|
|
78
|
+
const workflow = workflows[request.params.name];
|
|
77
79
|
if (!workflow || !request.params.arguments?.task)
|
|
78
80
|
throw new McpError(ErrorCode.InvalidParams, "Choose a workflow and provide task.");
|
|
79
81
|
return {
|
package/dist/tools.js
CHANGED
|
@@ -14,8 +14,10 @@ import { resolve, relative, isAbsolute, dirname, basename } from "node:path";
|
|
|
14
14
|
import { Ajv } from "ajv";
|
|
15
15
|
import { API_TOOLS } from "./generated-tools.js";
|
|
16
16
|
import { EXTRA_TOOLS, EXTRA_SCOPES, extraTool } from "./extra-tools.js";
|
|
17
|
-
import {
|
|
17
|
+
import { SEARCH_TERMS } from "./workflows.js";
|
|
18
18
|
import { PyAIError } from "./client.js";
|
|
19
|
+
import { hostedTools, AGENT_WRITE_TOOLS, withoutBindingConfig } from "./hosted-policy.js";
|
|
20
|
+
import { workflowsForMode, guideForMode } from "./workflows.js";
|
|
19
21
|
import { openapiGet } from "./contract.js";
|
|
20
22
|
const SPEECH_CALLING_GUIDE = "https://pyai.com/agents/speech-calling.md";
|
|
21
23
|
export const localContext = {
|
|
@@ -45,6 +47,28 @@ function text(value) {
|
|
|
45
47
|
: { text: s },
|
|
46
48
|
};
|
|
47
49
|
}
|
|
50
|
+
// Keep authorized resource IDs available to integrations without filling the
|
|
51
|
+
// default conversational text with identity metadata. Never return credential fields.
|
|
52
|
+
function identityResult(value, verbose) {
|
|
53
|
+
function safe(item) {
|
|
54
|
+
if (Array.isArray(item))
|
|
55
|
+
return item.map(safe);
|
|
56
|
+
if (!item || typeof item !== "object")
|
|
57
|
+
return item;
|
|
58
|
+
return Object.fromEntries(Object.entries(item).filter(([key, entry]) => !/secret|password|authorization|access_?token|refresh_?token|api_?key|token_ref/i.test(key)
|
|
59
|
+
&& !(typeof entry === "string" && /^(key|token)$/i.test(key))).map(([key, entry]) => [key, safe(entry)]));
|
|
60
|
+
}
|
|
61
|
+
const identity = safe(value);
|
|
62
|
+
const summary = Object.fromEntries([
|
|
63
|
+
"env", "status", "org_status", "scopes", "expires_at", "plan", "limits", "credit", "key_budget_cents",
|
|
64
|
+
].filter(key => Object.hasOwn(identity, key)).map(key => [key, identity[key]]));
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: JSON.stringify(verbose ? identity : {
|
|
67
|
+
...summary, details: "Authorized resource IDs are in structuredContent. Use verbose=true to include identity details in text.",
|
|
68
|
+
}, null, 2) }],
|
|
69
|
+
structuredContent: identity,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
48
72
|
/** Tools that need a configured key. create_sandbox_key + get_started do not. */
|
|
49
73
|
const KEY_REQUIRED = new Set([
|
|
50
74
|
"list_models",
|
|
@@ -100,10 +124,10 @@ const LEGACY_TOOLS = [
|
|
|
100
124
|
{
|
|
101
125
|
name: "whoami",
|
|
102
126
|
description: "Introspect the active key (GET /v1/me): org, environment (test/live), granted scopes, " +
|
|
103
|
-
"and rate-limit/credit posture. Use it to self-diagnose a 401/403/402.",
|
|
127
|
+
"and rate-limit/credit posture. Text is concise; authorized IDs remain in structuredContent. Use verbose=true for identity details. Use it to self-diagnose a 401/403/402.",
|
|
104
128
|
inputSchema: {
|
|
105
129
|
type: "object",
|
|
106
|
-
properties: {},
|
|
130
|
+
properties: { verbose: { type: "boolean", description: "Include authorized identity details in text. Credentials are always omitted.", default: false } },
|
|
107
131
|
additionalProperties: false,
|
|
108
132
|
},
|
|
109
133
|
},
|
|
@@ -415,7 +439,7 @@ export const TOOLS = [
|
|
|
415
439
|
destructiveHint: ["configure_amd", "enable_recap"].includes(t.name),
|
|
416
440
|
idempotentHint: readOnly.has(t.name),
|
|
417
441
|
openWorldHint: !readOnly.has(t.name) &&
|
|
418
|
-
!["synthesize_speech", "
|
|
442
|
+
!["synthesize_speech", "preview_cast_line", "transcribe_audio"].includes(t.name),
|
|
419
443
|
},
|
|
420
444
|
_meta: {
|
|
421
445
|
"pyai/docs_url": SPEECH_CALLING_GUIDE,
|
|
@@ -424,8 +448,10 @@ export const TOOLS = [
|
|
|
424
448
|
: [{ type: "noauth" }],
|
|
425
449
|
},
|
|
426
450
|
}));
|
|
451
|
+
export const HOSTED_TOOLS = hostedTools(TOOLS);
|
|
427
452
|
const ajv = new Ajv({ strict: false, allErrors: true, validateFormats: false });
|
|
428
453
|
const validators = new Map(TOOLS.map((t) => [t.name, ajv.compile(t.inputSchema)]));
|
|
454
|
+
const hostedValidators = new Map(HOSTED_TOOLS.map(t => [t.name, ajv.compile(t.inputSchema)]));
|
|
429
455
|
function str(args, key) {
|
|
430
456
|
const v = args[key];
|
|
431
457
|
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
@@ -445,10 +471,11 @@ export async function callTool(name, args, client, ctx = localContext) {
|
|
|
445
471
|
} };
|
|
446
472
|
}
|
|
447
473
|
async function executeTool(name, args, client, ctx) {
|
|
448
|
-
const
|
|
474
|
+
const availableTools = ctx.mode === "http" ? HOSTED_TOOLS : TOOLS;
|
|
475
|
+
const tool = availableTools.find((t) => t.name === name);
|
|
449
476
|
if (!tool)
|
|
450
477
|
return toolError(`Unknown tool: ${name}`, "unknown_tool");
|
|
451
|
-
const validator = validators.get(name);
|
|
478
|
+
const validator = (ctx.mode === "http" ? hostedValidators : validators).get(name);
|
|
452
479
|
if (!validator(args))
|
|
453
480
|
return toolError(`Invalid arguments: ${ajv.errorsText(validator.errors, { separator: "; " })}`, "invalid_arguments");
|
|
454
481
|
if (name === "create_sandbox_key" && ctx.mode === "http")
|
|
@@ -464,7 +491,12 @@ async function executeTool(name, args, client, ctx) {
|
|
|
464
491
|
},
|
|
465
492
|
};
|
|
466
493
|
if (name === "discover_tools") {
|
|
467
|
-
const
|
|
494
|
+
const query = typeof args.query === "string" ? args.query.trim() : undefined;
|
|
495
|
+
const task = typeof args.task === "string" ? args.task.trim() : undefined;
|
|
496
|
+
if (query === "" || task === "" || (query !== undefined && task !== undefined && query !== task)) {
|
|
497
|
+
return toolError("Use a non-empty query or task. If both are supplied, they must match after trimming.", "invalid_arguments");
|
|
498
|
+
}
|
|
499
|
+
const words = String(query ?? task ?? "")
|
|
468
500
|
.toLowerCase()
|
|
469
501
|
.match(/[a-z0-9]+/g)
|
|
470
502
|
?.filter((w) => ![
|
|
@@ -483,7 +515,8 @@ async function executeTool(name, args, client, ctx) {
|
|
|
483
515
|
"i",
|
|
484
516
|
"want",
|
|
485
517
|
].includes(w)) ?? [];
|
|
486
|
-
const
|
|
518
|
+
const workflows = workflowsForMode(ctx.mode);
|
|
519
|
+
const matches = Object.entries(workflows)
|
|
487
520
|
.map(([key, w]) => ({
|
|
488
521
|
key,
|
|
489
522
|
w,
|
|
@@ -496,11 +529,11 @@ async function executeTool(name, args, client, ctx) {
|
|
|
496
529
|
.sort((a, b) => b.score - a.score);
|
|
497
530
|
const selected = matches.length
|
|
498
531
|
? matches
|
|
499
|
-
: Object.entries(
|
|
532
|
+
: Object.entries(workflows).map(([key, w]) => ({ key, w, score: 0 }));
|
|
500
533
|
const names = new Set(selected.flatMap((m) => [...m.w.tools]));
|
|
501
534
|
return text({
|
|
502
535
|
workflows: selected.map(({ key, w }) => ({ product: key, ...w })),
|
|
503
|
-
tools:
|
|
536
|
+
tools: availableTools.filter((t) => names.has(t.name)).map((t) => ({
|
|
504
537
|
name: t.name,
|
|
505
538
|
description: t.description,
|
|
506
539
|
})),
|
|
@@ -526,7 +559,20 @@ async function executeTool(name, args, client, ctx) {
|
|
|
526
559
|
for (const key of generated.route.query)
|
|
527
560
|
if (args[key] !== undefined)
|
|
528
561
|
query.set(key, String(args[key]));
|
|
529
|
-
|
|
562
|
+
let requestBody = args.body;
|
|
563
|
+
if (ctx.mode === "http" && AGENT_WRITE_TOOLS.has(name) && name !== "create_agent") {
|
|
564
|
+
const bindings = name === "set_agent_tools" ? args.body : args.body?.tools;
|
|
565
|
+
if (Array.isArray(bindings)) {
|
|
566
|
+
// Fetch privately: do not route saved connection settings through model context.
|
|
567
|
+
const existing = await client.getJson(`/v1/agents/${encodeURIComponent(String(args.id))}`);
|
|
568
|
+
const saved = new Map((existing.tools ?? []).map(b => [b.tool_id, b.config]));
|
|
569
|
+
const preserved = bindings.map((b) => ({ ...b, ...(saved.has(b.tool_id) ? { config: saved.get(b.tool_id) } : {}) }));
|
|
570
|
+
requestBody = name === "set_agent_tools" ? preserved : { ...args.body, tools: preserved };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const result = await client.json(generated.route.method, path + (query.size ? "?" + query.toString() : ""), requestBody, "idempotency" in generated.route ? { "Idempotency-Key": String(args.idempotency_key) } : undefined);
|
|
574
|
+
return text(ctx.mode === "http" && ["list_agents", "get_agent", ...AGENT_WRITE_TOOLS].includes(name)
|
|
575
|
+
? withoutBindingConfig(result) : result);
|
|
530
576
|
}
|
|
531
577
|
const extra = await extraTool(name, args, client, ctx);
|
|
532
578
|
if (extra)
|
|
@@ -535,7 +581,7 @@ async function executeTool(name, args, client, ctx) {
|
|
|
535
581
|
case "openapi_get":
|
|
536
582
|
return text(await openapiGet(client, args));
|
|
537
583
|
case "get_started":
|
|
538
|
-
return text(
|
|
584
|
+
return text(guideForMode(ctx.mode));
|
|
539
585
|
case "create_sandbox_key": {
|
|
540
586
|
const label = str(args, "label");
|
|
541
587
|
const result = await client.postJson("/v1/sandbox/keys", label ? { label } : {});
|
|
@@ -549,7 +595,7 @@ async function executeTool(name, args, client, ctx) {
|
|
|
549
595
|
});
|
|
550
596
|
}
|
|
551
597
|
case "whoami":
|
|
552
|
-
return
|
|
598
|
+
return identityResult(await client.getJson("/v1/me"), args.verbose === true);
|
|
553
599
|
case "list_models":
|
|
554
600
|
return text(await client.getJson("/v1/models"));
|
|
555
601
|
case "list_voices": {
|
package/dist/workflows.js
CHANGED
|
@@ -144,3 +144,22 @@ export const SEARCH_TERMS = {
|
|
|
144
144
|
dub: "dubbing translate translation languages audio",
|
|
145
145
|
cast: "narration direct expressive script emotion podcast",
|
|
146
146
|
};
|
|
147
|
+
// Keep every hosted discovery surface consistent with the callable catalog.
|
|
148
|
+
export function workflowsForMode(mode) {
|
|
149
|
+
if (mode === "stdio")
|
|
150
|
+
return WORKFLOWS;
|
|
151
|
+
return { ...WORKFLOWS, calling: { ...WORKFLOWS.calling,
|
|
152
|
+
tools: WORKFLOWS.calling.tools.filter(name => name !== "telephony_buy_number"),
|
|
153
|
+
steps: [WORKFLOWS.calling.steps[0],
|
|
154
|
+
"Use an already-owned number. Number purchasing is unavailable through hosted MCP. Binding changes routing; outbound calls require explicit destination approval, confirm=true and a stable idempotency_key.",
|
|
155
|
+
...WORKFLOWS.calling.steps.slice(2)],
|
|
156
|
+
} };
|
|
157
|
+
}
|
|
158
|
+
export function guideForMode(mode) {
|
|
159
|
+
return mode === "stdio" ? GUIDE : GUIDE + "\nHosted MCP does not purchase numbers or accept connection credentials. Use saved tool IDs; configure connections privately in the PyAI console. Supply only recordings and text you are authorized to process. Do not submit payment-card data, protected health information, government identifiers, or authentication secrets.\n";
|
|
160
|
+
}
|
|
161
|
+
export function instructionsForMode(mode) {
|
|
162
|
+
return mode === "stdio" ? SERVER_INSTRUCTIONS : SERVER_INSTRUCTIONS
|
|
163
|
+
.replace("purchases and real outbound calls need explicit authorization", "real outbound calls need explicit destination authorization; number purchasing is unavailable here")
|
|
164
|
+
.replace(" Current pricing: https://pyai.com/pricing.", " Configure connection credentials privately in the PyAI console; use saved tool IDs here. Do not request restricted personal data or direct users to checkout.");
|
|
165
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyai/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "PyAI MCP: OAuth-connected voice agents, speech, transcription, compliance, call summaries, dubbing and expressive narration for chat apps and coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|