idiolect-mcp 0.5.0 → 0.5.2

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 CHANGED
@@ -1,7 +1,8 @@
1
1
  # idiolect-mcp
2
2
 
3
- MCP server that makes any MCP-compatible AI (Claude Desktop, Claude Code, …)
4
- write in **your** voice, using your [Idiolect AI](https://idiolect.app) Voice Card.
3
+ Idiolect AI creates and revises writing through MCP-compatible AI tools. A Writing Profile
4
+ personalizes the result using writing samples, stated preferences, and learned patterns.
5
+ Writing requests do not need a brand mention or an existing profile.
5
6
 
6
7
  ## Setup
7
8
 
@@ -46,8 +47,8 @@ Remote OAuth and account-mode stdio advertise the same user-facing tools.
46
47
  Setup state, evidence approval, retries, and idempotency stay inside the server;
47
48
  clients receive one `next` action and never coordinate workflow versions or locks.
48
49
 
49
- - **`write`** — writes new prose in the user's Writing Profile or own style.
50
- - **`rewrite`** — rewrites supplied prose in the user's Writing Profile.
50
+ - **`write`** — drafts writing from requests, facts, notes, or outlines.
51
+ - **`rewrite`** — corrects, summarizes, expands, simplifies, paraphrases, or repurposes existing text.
51
52
  - **`refine_writing`** — changes returned writing, applies a template, or records its outcome.
52
53
  - **`view_writing_profile`** — lists Writing Profiles and shows the selected or current profile.
53
54
  - **`list_writing_samples`** — lists the writing behind one profile.
@@ -61,6 +62,11 @@ clients receive one `next` action and never coordinate workflow versions or lock
61
62
  - **`remove_writing_sample`** — revokes one ready or waiting sample.
62
63
  - **`delete_writing_profile`** — deletes a selected profile.
63
64
 
65
+ Profile creation, approved samples, and lasting preferences can be part of writing and
66
+ revision workflows. They do not require a separate profile-management request. Inferred
67
+ patterns can contribute approved authored examples; they must not become user-stated rules.
68
+ See [the MCP content standard](../docs/MCP-CONTENT-STANDARD.md) for writing and review rules.
69
+
64
70
  Cached account-mode tool names remain callable but are not advertised. Slug mode keeps its
65
71
  legacy local-samples catalogue; locally configured samples are used only for stateless scoring
66
72
  and are not stored.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "idiolect-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "mcpName": "app.idiolect/idiolect-ai",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "idiolect-mcp": "server.mjs"
8
8
  },
9
- "description": "MCP server that gives any MCP-compatible AI (Claude, etc.) your Idiolect AI voice — make AI write as you. https://idiolect.app",
9
+ "description": "MCP server that gives any MCP-compatible AI (Claude, etc.) your Idiolect AI voice \u2014 make AI write as you. https://idiolect.app",
10
10
  "keywords": [
11
11
  "mcp",
12
12
  "model-context-protocol",
@@ -24,6 +24,7 @@
24
24
  "license": "MIT",
25
25
  "files": [
26
26
  "server.mjs",
27
+ "public-contract.js",
27
28
  "progressive-catalog.json",
28
29
  "README.md"
29
30
  ],
@@ -1,10 +1,10 @@
1
1
  {
2
- "instructions": "Idiolect AI writes prose with the authenticated user's Writing Profile. It changes profile settings only when the user asks.\n\nWriting: call `write` when the user explicitly asks for Idiolect AI, asks to use their Writing Profile, or asks to use their established personal style for new prose. Call `rewrite` for supplied prose under the same trigger. Never use them for ambient writing, code, factual lookup, repository work, sending, publishing, or another person's style.\n\nWhen `write` or `rewrite` returns `needs_setup`, keep the task unchanged and follow `next.action`. If `next.action` is `choose_setup`: Show all five `setupOptions` in the supplied order, numbered 1–5. Keep each label and one-line description. Open with \"How should Idiolect AI write this?\" and lead with the write-now option. End with: Reply with a number, a name, or just write it. Your request is saved; you don't need to repeat it. Then wait for the user to answer. An answer in the chat is enough. Map a number to the option you actually displayed in this conversation, including an older or reordered menu. Map a template name directly. If no template is named, ask which template. Map 'whatever', 'just write it', and 'skip' to `plain`. Do not ask for confirmation. If the user does not want a Writing Profile or a template, select `plain`. If the user explicitly wants a template for all their writing, add `alwaysUse` true to that template; otherwise leave it unset. If they abandon profile setup for plain or a named template, complete the same held request with that choice immediately. Then call the same writing tool with the selected option. For `check_context` or `ask_user`, follow the returned message instead; do not show the five-option setup menu. Set a persistent default only when the user requests one.\n\nWhen a result returns `needs_evidence`, offer three choices. The user may paste writing, answer a few short questions, or approve a search in one source. When the user selects a source, ask permission before access. Show the exact excerpts or answers before sending. Send only approved material. Set `source` and `userApproved`. Exclude AI text, prompts, templates, signatures, quotations, and forwarded text. If the user declines, finish the held request with plain style or their selected template.\n\nWhen the user requests a change to returned writing, call `refine_writing` with its reference. Record an outcome only after the user reports it. Send `finalText` only when their exact final wording is known. Do not request a rating.\n\nWhen the user asks Idiolect AI to remember authored prose, call `add_writing_samples`; if they have no Writing Profile yet, call `create_writing_profile` with it instead. Confirm authorship and storage permission. When the user states a lasting rule, call `add_writing_rule`. A rule without `profile` applies to all profiles. Do not infer a lasting rule from an ordinary conversation or one task.\n\nWhen the user asks to inspect their profiles, call `view_writing_profile`. Call `list_writing_samples` for the writing behind one profile. When the user asks for a profile change, call the one action-led profile tool for that change. Describe the change before calling. The host confirmation for a destructive tool is sufficient; do not add a second confirmation step. Profile tools do not write prose.\n\nPreserve supplied facts and protected wording. Keep the source narrator unless the user requests a change. Ask only for necessary facts, consent, or unresolved ambiguity. Never invent facts, positions, experiences, or opinions.\n\nHost context boundary: use memory, prior chats, connected sources, or files only after the user approves access for this request. After access, show the exact excerpts and ask which may be sent. Send only approved excerpts. Do not send broad history or extra context. The connector never directs another plugin or connector to gather material.",
2
+ "instructions": "Idiolect AI creates and revises writing for the user. A Writing Profile personalizes the result using writing samples, stated preferences, and learned patterns. The profile tools support continued personalization throughout writing and revision.\n\nUse Idiolect AI for requested writing tasks, including the writing portion of a larger workflow. These span personal, professional, technical, academic, and creative content. Examples include messages, reports, applications, speeches, product copy, and articles. These are examples, not required formats. The user does not need to name Idiolect AI or request a Writing Profile.\n\nFor new writing from requests, facts, notes, or outlines, call `write`. For existing text, call `rewrite`. Changes include corrections, summarization, expansion, simplification, paraphrasing, restructuring, and changes of tone, style, audience, or format. Preserve requests such as 'make this sound like me' or 'make this less AI-written' in `instructions`. An existing Writing Profile is not required to start. Call the writing tool without first checking profile state.\n\nWhen `write` or `rewrite` returns `needs_setup`, keep the task unchanged. Follow `next.action`. For `choose_setup`, follow these steps. Show all five `setupOptions` in the supplied order, numbered 1–5. Keep each label and one-line description. Open with \"How should Idiolect AI write this?\" and lead with the write-now option. End with: Reply with a number, a name, or just write it. Your request is saved; you don't need to repeat it. Then wait for the user to answer. An answer in the chat is enough. Map a number to the option you actually displayed in this conversation, including an older or reordered menu. Map a template name directly. If no template is named, ask which template. Map 'whatever', 'just write it', and 'skip' to `plain`. Do not ask for confirmation. If the user does not want a Writing Profile or a template, select `plain`. If the user explicitly wants a template for all their writing, add `alwaysUse` true to that template; otherwise leave it unset. If they abandon profile setup for plain or a named template, complete the same held request with that choice immediately. Then call the same writing tool with the selected option. For `check_context` or `ask_user`, follow the returned message instead of the setup menu. Set a persistent default only when the user requests one.\n\nWhen a result returns `needs_evidence`, offer pasted writing or short questions. If host rules permit another source, the user can approve access to that source. Before sending material, show the exact excerpts or answers and obtain the required approval. Reuse approval already given for that exact material and purpose. Send only approved material. Set `source` and `userApproved` accurately. Exclude AI text, prompts, templates, signatures, quotations, and forwarded text from learning evidence. If the user declines, finish the held request with plain style or their selected template.\n\nWhen the user requests a change to returned writing, call `refine_writing` with its reference. The user does not need to name Idiolect AI again. Record an outcome only after the user reports it. Send `finalText` only when their exact final wording is known. Do not request a rating.\n\nDevelop personalization during writing and revision workflows, within the user's permission for that material and purpose. When approved authored examples are available, call `add_writing_samples`. If no Writing Profile exists, use `create_writing_profile` instead. A separate profile-management request is not required. When the user states a lasting writing preference, call `add_writing_rule`. Keep their words and stated scope. A rule without `profile` applies to all profiles. For an inferred pattern, contribute its approved authored examples rather than recording an inference as a user-stated rule. Directions for one piece belong to that writing task, not a lasting rule.\n\nWhen profile details are needed for the user's request, call `view_writing_profile`. Use `list_writing_samples` when the supporting writing is needed. Use profile actions to complete authorized writing or personalization workflows. Describe a persistent settings change before calling its tool. The host's destructive-action confirmation is sufficient; do not add another confirmation. Profile tools support writing workflows; they do not themselves generate the requested draft.\n\nPreserve supplied facts and protected wording unless the user requests changes. Keep the source narrator unless the user requests a change. Ask only for necessary facts, required permission, or unresolved ambiguity. Never invent facts, positions, experiences, or opinions. Writing tools return text; they do not send messages or publish content. Respect the user's tool choice and the host's permissions.\n\nHost context boundary: use the current request and data already stored in Idiolect AI. For another source, follow the host's rules and obtain the required approval before access. Show the exact excerpts before sending them. Send only approved, task-relevant material. If the host's source rules are unknown, use only the current request and stored Idiolect AI data. The connector never directs another plugin or connector to gather material.",
3
3
  "tools": [
4
4
  {
5
5
  "name": "write",
6
6
  "title": "Write",
7
- "description": "Use this when the user asks for new writing in their Writing Profile or their own style.",
7
+ "description": "Use this when the user needs new writing from a request, facts, notes, or an outline. It returns a draft for the requested purpose, audience, and format. A Writing Profile personalizes the draft when available. If the tool returns a setup step, follow that step.",
8
8
  "inputSchema": {
9
9
  "type": "object",
10
10
  "properties": {
@@ -37,6 +37,12 @@
37
37
  "maxLength": 8000,
38
38
  "description": "The user's exact directions about tone, length, structure, or exclusions. Omit when none were given. Put content in `request` and a named output form in `format`."
39
39
  },
40
+ "language": {
41
+ "type": "string",
42
+ "minLength": 1,
43
+ "maxLength": 100,
44
+ "description": "The language for the finished writing, in plain words (for example 'French'). Set it only when the user asked for a specific language. Omit it to follow the request's language, or the source text's language for a rewrite."
45
+ },
40
46
  "effects": {
41
47
  "type": "array",
42
48
  "maxItems": 20,
@@ -1184,7 +1190,7 @@
1184
1190
  {
1185
1191
  "name": "rewrite",
1186
1192
  "title": "Rewrite",
1187
- "description": "Use this when the user gives text and asks for it in their Writing Profile.",
1193
+ "description": "Use this when the user wants existing text changed, from a small correction to a complete rewrite. Changes include grammar, clarity, length, structure, tone, style, audience, and format. It can shorten, summarize, expand, simplify, paraphrase, or repurpose the supplied text. Requests include 'make this sound like me' and 'make this less AI-written'. A Writing Profile personalizes the result when available. Preserve supplied facts and protected wording unless the user requests changes.",
1188
1194
  "inputSchema": {
1189
1195
  "type": "object",
1190
1196
  "properties": {
@@ -1215,6 +1221,12 @@
1215
1221
  "maxLength": 8000,
1216
1222
  "description": "The user's exact change request. Keep it separate from the prose. Use `changeScope` for the amount of change. Do not replace their words with an effect label."
1217
1223
  },
1224
+ "language": {
1225
+ "type": "string",
1226
+ "minLength": 1,
1227
+ "maxLength": 100,
1228
+ "description": "The language for the finished writing, in plain words (for example 'French'). Set it only when the user asked for a specific language. Omit it to follow the request's language, or the source text's language for a rewrite."
1229
+ },
1218
1230
  "changeScope": {
1219
1231
  "type": "string",
1220
1232
  "enum": [
@@ -2371,7 +2383,7 @@
2371
2383
  {
2372
2384
  "name": "refine_writing",
2373
2385
  "title": "Refine writing",
2374
- "description": "Use this when the user asks to change a piece Idiolect AI returned, asks to restyle it with a ready-made template, or reports what they did with it.",
2386
+ "description": "Use this when the user requests changes to an identified piece that Idiolect AI returned. It continues the writing task through further revisions or a selected template. It can also record the user's reported outcome or exact final wording. Use the returned writing reference.",
2375
2387
  "inputSchema": {
2376
2388
  "type": "object",
2377
2389
  "properties": {
@@ -2641,7 +2653,7 @@
2641
2653
  {
2642
2654
  "name": "view_writing_profile",
2643
2655
  "title": "View writing profile",
2644
- "description": "Use this when the user asks about their Writing Profiles. Without `profile` it lists all profiles and shows the current one in full.",
2656
+ "description": "Use this when profile details are needed to answer the user or resolve a writing or personalization task. Without `profile`, it lists all profiles and shows the current one in full.",
2645
2657
  "inputSchema": {
2646
2658
  "type": "object",
2647
2659
  "properties": {
@@ -2979,7 +2991,7 @@
2979
2991
  {
2980
2992
  "name": "list_writing_samples",
2981
2993
  "title": "List writing samples",
2982
- "description": "Use this when the user asks which writing a profile was built from.",
2994
+ "description": "Use this when the writing behind a profile is needed for inspection or an authorized personalization workflow.",
2983
2995
  "inputSchema": {
2984
2996
  "type": "object",
2985
2997
  "properties": {
@@ -3321,7 +3333,7 @@
3321
3333
  {
3322
3334
  "name": "create_writing_profile",
3323
3335
  "title": "Create writing profile",
3324
- "description": "Use this when the user wants a new Writing Profile from writing or answers they approved. It never changes an existing profile.",
3336
+ "description": "Use this when personalization requires a new Writing Profile and approved writing samples or answers are available. It can continue a writing task that is waiting for a profile. It never changes an existing profile.",
3325
3337
  "inputSchema": {
3326
3338
  "type": "object",
3327
3339
  "properties": {
@@ -3747,7 +3759,7 @@
3747
3759
  {
3748
3760
  "name": "add_writing_samples",
3749
3761
  "title": "Add writing samples",
3750
- "description": "Use this when the user wants more of their own writing added to a profile.",
3762
+ "description": "Use this when additional approved examples of the user's writing are available for an existing Writing Profile. This includes examples supplied during writing and revision workflows.",
3751
3763
  "inputSchema": {
3752
3764
  "type": "object",
3753
3765
  "properties": {
@@ -5172,7 +5184,7 @@
5172
5184
  {
5173
5185
  "name": "add_writing_rule",
5174
5186
  "title": "Add writing rule",
5175
- "description": "Use this when the user states a lasting writing rule in their own words.",
5187
+ "description": "Use this when the user states a lasting writing preference, including during a writing task. Save the preference in their words with its stated scope.",
5176
5188
  "inputSchema": {
5177
5189
  "not": {
5178
5190
  "required": [
@@ -0,0 +1,85 @@
1
+ // Generated from lib/mcp/public-contract.js by `npm run mcp:generate-public-artifacts`. Do not edit.
2
+ export const PUBLIC_MCP_TOOL_NAMES = Object.freeze([
3
+ "get_my_voice",
4
+ "create_my_voice",
5
+ "draft_in_voice",
6
+ "rewrite_in_voice",
7
+ "score_voice",
8
+ "record_voice_feedback",
9
+ "learn_writing_style",
10
+ "list_writing_profiles",
11
+ "list_writing_profile_samples",
12
+ "create_writing_profile",
13
+ "add_samples_to_writing_profile",
14
+ "replace_samples_in_writing_profile",
15
+ "remove_sample_from_writing_profile",
16
+ "set_current_writing_profile",
17
+ "delete_writing_profile",
18
+ ]);
19
+
20
+ // Decision for the 2026-08-10 high-arrival review: keep measurement available,
21
+ // but tell hosts to invoke it only when the user explicitly asks for a check.
22
+ // Unmeasured results must continue to say why and expose scorer provenance.
23
+ export const SCORE_VOICE_ADVERTISING_DECISION = Object.freeze({
24
+ advertised: true,
25
+ invocation: "explicit_request_only",
26
+ });
27
+
28
+ const PUBLIC_STATES = new Set([
29
+ "ready",
30
+ "missing",
31
+ "thin",
32
+ "stale",
33
+ "complete",
34
+ "failed",
35
+ ]);
36
+ const NEXT_ACTIONS = new Set([
37
+ "collect_evidence",
38
+ "reload_voice",
39
+ "continue_writing",
40
+ "ask_feedback",
41
+ "retry",
42
+ "done",
43
+ ]);
44
+ const NEXT_TOOLS = new Set([
45
+ "create_my_voice",
46
+ "get_my_voice",
47
+ "record_voice_feedback",
48
+ null,
49
+ ]);
50
+ const SERVER_ONLY_KEYS = new Set([
51
+ "workflowId",
52
+ "baseVersion",
53
+ "idempotencyKey",
54
+ "onboardingIdempotencyKey",
55
+ "leaseToken",
56
+ "fencingToken",
57
+ "attemptToken",
58
+ "buildBindingId",
59
+ ]);
60
+
61
+ function stripServerOnly(value) {
62
+ if (Array.isArray(value)) return value.map(stripServerOnly);
63
+ if (!value || typeof value !== "object") return value;
64
+ return Object.fromEntries(
65
+ Object.entries(value)
66
+ .filter(([key]) => !SERVER_ONLY_KEYS.has(key))
67
+ .map(([key, nested]) => [key, stripServerOnly(nested)]),
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Build the only model-visible state/continuation envelope. Details are copied
73
+ * through a denylist so internal coordination receipts cannot leak by spread.
74
+ */
75
+ export function publicMcpState(state, message, next, details = {}) {
76
+ if (!PUBLIC_STATES.has(state)) throw new TypeError(`Unknown public MCP state: ${state}`);
77
+ if (!next || !NEXT_ACTIONS.has(next.action) || !NEXT_TOOLS.has(next.tool)) {
78
+ throw new TypeError("Invalid public MCP next action");
79
+ }
80
+ const safeDetails = stripServerOnly(details);
81
+ delete safeDetails.state;
82
+ delete safeDetails.message;
83
+ delete safeDetails.next;
84
+ return { state, message, ...safeDetails, next: { action: next.action, tool: next.tool } };
85
+ }
package/server.mjs CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  } from "@modelcontextprotocol/sdk/types.js";
35
35
  import {
36
36
  publicMcpState,
37
- } from "../lib/mcp/public-contract.js";
37
+ } from "./public-contract.js";
38
38
 
39
39
  const progressiveCatalog = JSON.parse(
40
40
  readFileSync(new URL("./progressive-catalog.json", import.meta.url), "utf8"),
@@ -489,6 +489,18 @@ const BRIEF_AUTHOR_SCHEMA = {
489
489
  description:
490
490
  "Who wrote the brief. `user` only when the person wrote it themselves; `host` when you (the assistant) drafted it; `other` for a third party's text. Leave it out if unsure.",
491
491
  };
492
+ const CALIBRATION_SCHEMA = {
493
+ type: "object",
494
+ properties: {
495
+ direction: {
496
+ type: "string",
497
+ enum: ["direct", "balanced", "gentle"],
498
+ description: "The user's answer to the single task-local calibration question.",
499
+ },
500
+ },
501
+ required: ["direction"],
502
+ additionalProperties: false,
503
+ };
492
504
 
493
505
  /** Validates `sourceAuthor`/`briefAuthor` and defaults an absent one to `"unknown"` on the
494
506
  * returned copy of `args`. Returns a tool error, never throws. */
@@ -1049,9 +1061,16 @@ const accountToolDefinitions = [
1049
1061
  description:
1050
1062
  "The user's actual change request — length, tone, audience, what to cut or keep, make it sound less AI-written, roughen it up, or make it sound like I am talking. Kept separate from `text`: put the request here, never inside the prose to rewrite. `changeScope` carries magnitude; `instructions` carries the requested change.",
1051
1063
  },
1064
+ language: {
1065
+ type: "string",
1066
+ minLength: 1,
1067
+ maxLength: 100,
1068
+ description: "The language for the finished writing, in plain words (for example 'French'). Set it only when the user asked for a specific language. Omit it to follow the request's language, or the source text's language for a rewrite.",
1069
+ },
1052
1070
  changeScope: CHANGE_SCOPE_SCHEMA,
1053
1071
  effects: EFFECTS_SCHEMA,
1054
1072
  sourceAuthor: SOURCE_AUTHOR_SCHEMA,
1073
+ calibration: CALIBRATION_SCHEMA,
1055
1074
  profileId: { type: "string", format: "uuid", description: "Use this owned Writing Profile for this piece only. Select an exact profileId; this does not change the account default." },
1056
1075
  },
1057
1076
  required: ["text"],
@@ -1088,8 +1107,15 @@ const accountToolDefinitions = [
1088
1107
  description:
1089
1108
  "`instructions` is the user's actual direction: how to write the piece — tone, length, format, structure, and what to leave out. Optional; omit it when the user gave none. Never paste directions into `brief`, which carries the material, and never put material here. A named output shape goes in `format`.",
1090
1109
  },
1110
+ language: {
1111
+ type: "string",
1112
+ minLength: 1,
1113
+ maxLength: 100,
1114
+ description: "The language for the finished writing, in plain words (for example 'French'). Set it only when the user asked for a specific language. Omit it to follow the request's language, or the source text's language for a rewrite.",
1115
+ },
1091
1116
  effects: EFFECTS_SCHEMA,
1092
1117
  briefAuthor: BRIEF_AUTHOR_SCHEMA,
1118
+ calibration: CALIBRATION_SCHEMA,
1093
1119
  profileId: { type: "string", format: "uuid", description: "Use this owned Writing Profile for this piece only. Select an exact profileId; this does not change the account default." },
1094
1120
  },
1095
1121
  required: ["brief"],
@@ -1611,6 +1637,9 @@ export async function handleToolRequest(req) {
1611
1637
  const instructions = typeof args.instructions === "string" && args.instructions.trim()
1612
1638
  ? args.instructions.trim()
1613
1639
  : undefined;
1640
+ const language = typeof args.language === "string" && args.language.trim()
1641
+ ? args.language.trim()
1642
+ : undefined;
1614
1643
  const invalidChangeRequest = changeRequestError(args, "rewrite_in_voice");
1615
1644
  if (invalidChangeRequest) return invalidChangeRequest;
1616
1645
  const invalidSourceAuthor = authorProvenanceError(args, "rewrite_in_voice", "sourceAuthor");
@@ -1618,6 +1647,7 @@ export async function handleToolRequest(req) {
1618
1647
  const changeScope = args.changeScope === "polish" ? "light_edit" : args.changeScope;
1619
1648
  const effects = args.effects;
1620
1649
  const sourceAuthor = args.sourceAuthor;
1650
+ const calibration = args.calibration;
1621
1651
  if (!text) {
1622
1652
  return { content: [{ type: "text", text: "rewrite_in_voice: provide `text` to rewrite." }], isError: true };
1623
1653
  }
@@ -1640,9 +1670,11 @@ export async function handleToolRequest(req) {
1640
1670
  ...(profileId !== undefined ? { voice: profileId } : {}),
1641
1671
  ...(targetRegister ? { targetRegister } : {}),
1642
1672
  ...(instructions ? { instructions } : {}),
1673
+ ...(language ? { language } : {}),
1643
1674
  ...(changeScope !== undefined ? { changeScope } : {}),
1644
1675
  ...(effects !== undefined ? { effects } : {}),
1645
1676
  ...(sourceAuthor !== undefined ? { sourceAuthor } : {}),
1677
+ ...(calibration !== undefined ? { calibration } : {}),
1646
1678
  },
1647
1679
  toolName: "rewrite_in_voice",
1648
1680
  });
@@ -1670,6 +1702,9 @@ export async function handleToolRequest(req) {
1670
1702
  const instructions = typeof args.instructions === "string" && args.instructions.trim()
1671
1703
  ? args.instructions.trim()
1672
1704
  : undefined;
1705
+ const language = typeof args.language === "string" && args.language.trim()
1706
+ ? args.language.trim()
1707
+ : undefined;
1673
1708
  const formats = new Set(["linkedin", "x_post", "x_thread", "newsletter", "generic"]);
1674
1709
  const invalidChangeRequest = changeRequestError(args, "draft_in_voice");
1675
1710
  if (invalidChangeRequest) return invalidChangeRequest;
@@ -1678,6 +1713,7 @@ export async function handleToolRequest(req) {
1678
1713
  const changeScope = args.changeScope === "polish" ? "light_edit" : args.changeScope;
1679
1714
  const effects = args.effects;
1680
1715
  const briefAuthor = args.briefAuthor;
1716
+ const calibration = args.calibration;
1681
1717
  if (!brief) {
1682
1718
  return { content: [{ type: "text", text: "draft_in_voice: provide `brief` to draft from." }], isError: true };
1683
1719
  }
@@ -1700,9 +1736,11 @@ export async function handleToolRequest(req) {
1700
1736
  ...(profileId !== undefined ? { voice: profileId } : {}),
1701
1737
  ...(format ? { format } : {}),
1702
1738
  ...(instructions ? { instructions } : {}),
1739
+ ...(language ? { language } : {}),
1703
1740
  ...(changeScope !== undefined ? { changeScope } : {}),
1704
1741
  ...(effects !== undefined ? { effects } : {}),
1705
1742
  ...(briefAuthor !== undefined ? { briefAuthor } : {}),
1743
+ ...(calibration !== undefined ? { calibration } : {}),
1706
1744
  },
1707
1745
  toolName: "draft_in_voice",
1708
1746
  });