@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 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.
@@ -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: "Submit an audio URL for translation and dubbing. Consumes usage. Keep job_id and poll get_dub_job; do not submit again after a timeout.",
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: "Dubbing",
40
- description: "Check a submitted Dub job. Poll pending/processing states without resubmitting.",
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
  },
@@ -609,7 +609,7 @@ export const API_TOOLS = [
609
609
  "null"
610
610
  ],
611
611
  "maxLength": 200,
612
- "description": "TTS delivery direction for this managed Agent on instruct-capable voice tiers. Omit or set null for PyAI's natural conversational pace; an inline Omni `configure.voice_instruct` wins for that session."
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
- "description": "Requested language for this Agent's Omni sessions. `null` or absent means `en`. Availability is staged: as of 2026-08-13 public serving is `en`, `fr`, `es`, and `hi`; `de` falls back to English. Inspect the session's `configured.language_active` and `language_fallback` fields before assuming the requested language is active. See the Language support reference."
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": "TTS delivery direction for this managed Agent on instruct-capable voice tiers. Omit or set null for PyAI's natural conversational pace; an inline Omni `configure.voice_instruct` wins for that session."
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
- "description": "Requested language for this Agent's Omni sessions. `null` or absent means `en`. Availability is staged: as of 2026-08-13 public serving is `en`, `fr`, `es`, and `hi`; `de` falls back to English. Inspect the session's `configured.language_active` and `language_fallback` fields before assuming the requested language is active. See the Language support reference."
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 { GUIDE, RESOURCES, SERVER_INSTRUCTIONS, WORKFLOWS, } from "./workflows.js";
5
- export const MCP_VERSION = "0.4.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: SERVER_INSTRUCTIONS,
16
+ instructions: instructionsForMode(context.mode),
15
17
  });
16
18
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
17
- tools: TOOLS.filter((t) => context.mode === "stdio" || t.name !== "create_sandbox_key").map((t) => context.mode === "stdio"
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 (!TOOLS.some((t) => t.name === request.params.name) ||
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" ? GUIDE : JSON.stringify(WORKFLOWS),
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(WORKFLOWS).map(([name, w]) => ({
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 = WORKFLOWS[request.params.name];
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 { GUIDE, WORKFLOWS, SEARCH_TERMS } from "./workflows.js";
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", "create_dub", "preview_cast_line", "transcribe_audio"].includes(t.name),
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 tool = TOOLS.find((t) => t.name === name);
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 words = String(args.query ?? "")
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 matches = Object.entries(WORKFLOWS)
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(WORKFLOWS).map(([key, w]) => ({ key, w, score: 0 }));
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: TOOLS.filter((t) => names.has(t.name)).map((t) => ({
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
- return text(await client.json(generated.route.method, path + (query.size ? "?" + query.toString() : ""), args.body, "idempotency" in generated.route ? { "Idempotency-Key": String(args.idempotency_key) } : undefined));
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(GUIDE);
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 text(await client.getJson("/v1/me"));
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.4.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",