idiolect-mcp 0.5.1 → 0.6.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.
@@ -81,5 +81,16 @@ export function publicMcpState(state, message, next, details = {}) {
81
81
  delete safeDetails.state;
82
82
  delete safeDetails.message;
83
83
  delete safeDetails.next;
84
- return { state, message, ...safeDetails, next: { action: next.action, tool: next.tool } };
84
+ return {
85
+ state,
86
+ message,
87
+ ...safeDetails,
88
+ next: {
89
+ action: next.action,
90
+ tool: next.tool,
91
+ ...(next.action === "retry" && typeof next.retryAfterSeconds === "number"
92
+ ? { retryAfterSeconds: next.retryAfterSeconds }
93
+ : {}),
94
+ },
95
+ };
85
96
  }
package/server.mjs CHANGED
@@ -47,7 +47,16 @@ export const accountRuntimeCompatibilityToolNames = Object.freeze([
47
47
  "feedback",
48
48
  "refine",
49
49
  "profile",
50
- "manage_profile",
50
+ "list_writing_samples",
51
+ "create_writing_profile",
52
+ "add_writing_samples",
53
+ "rename_writing_profile",
54
+ "set_current_writing_profile",
55
+ "set_default_writing_template",
56
+ "add_writing_rule",
57
+ "remove_writing_rule",
58
+ "remove_writing_sample",
59
+ "delete_writing_profile",
51
60
  "learn_writing_style",
52
61
  "get_my_voice",
53
62
  "create_my_voice",
@@ -142,195 +151,14 @@ export function formatLegacyScoreVoiceResult(result) {
142
151
  return `${lines}\n\n(${provenance} — higher = reads more like you; not a forensic identity claim.)`;
143
152
  }
144
153
 
145
- const CREDIT_STATES = new Set([
146
- "free_beta",
147
- "balance_available",
148
- "low_balance",
149
- "zero_balance",
150
- "legacy_unlimited",
151
- "accounting_unavailable",
152
- ]);
153
- const CREDIT_MODES = new Set(["free", "credits"]);
154
-
155
154
  function plainObject(value) {
156
155
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
157
156
  }
158
157
 
159
- function safeManageUrl(value) {
160
- if (value === null) return null;
161
- if (typeof value !== "string") return undefined;
162
- try {
163
- const parsed = new URL(value);
164
- const accountOrigin = new URL(ACCOUNT_BASE).origin;
165
- const firstParty = parsed.hostname === "idiolect.app" || parsed.hostname.endsWith(".idiolect.app");
166
- if (
167
- !["https:", "http:"].includes(parsed.protocol)
168
- || (parsed.origin !== accountOrigin && !firstParty)
169
- || parsed.pathname !== "/settings"
170
- || parsed.username
171
- || parsed.password
172
- || parsed.search !== "?section=credits"
173
- || parsed.hash
174
- ) return undefined;
175
- return `${parsed.origin}/settings?section=credits`;
176
- } catch {
177
- return undefined;
178
- }
179
- }
180
-
181
- function creditMessage(credits) {
182
- const { state, balance, charged, manageUrl, mode } = credits;
183
- if (state === "free_beta") {
184
- return charged === 0
185
- ? "Free during beta. No credits are being used."
186
- : "Completed free during beta. No credit was used.";
187
- }
188
- if (state === "legacy_unlimited") return "Unlimited generation access is active. Credits are not used.";
189
- if (state === "accounting_unavailable") {
190
- return mode === "free"
191
- ? "Free during beta. The credit balance is temporarily unavailable; generation remains available."
192
- : "The credit balance is temporarily unavailable. Generation is paused; no credit was used.";
193
- }
194
- if (state === "zero_balance") {
195
- return charged === 1
196
- ? "This request completed with the last credit."
197
- : `The credit balance is 0. A draft or rewrite needs 1 credit. Credit management: ${manageUrl}.`;
198
- }
199
- if (charged === 1) return `1 credit used. ${balance} ${balance === 1 ? "remains" : "remain"}.`;
200
- if (state === "low_balance") return `${balance} credit${balance === 1 ? "" : "s"} remain. Credit management: ${manageUrl}.`;
201
- return `${balance} credits available. Each draft or rewrite uses 1 credit.`;
202
- }
203
-
204
- function validCreditInvariant(credits) {
205
- const {
206
- state,
207
- mode,
208
- balance,
209
- balancePreview,
210
- unitCost,
211
- charged,
212
- unlimited,
213
- accountingAvailable,
214
- generationAvailable,
215
- purchaseRequired,
216
- manageUrl,
217
- } = credits;
218
- if (state === "free_beta") {
219
- return mode === "free" && balance !== null && balancePreview && unitCost === 0 && charged === 0
220
- && !unlimited && accountingAvailable && generationAvailable && !purchaseRequired && manageUrl === null;
221
- }
222
- if (state === "legacy_unlimited") {
223
- return mode === "credits" && balance === null && !balancePreview && unitCost === 0 && charged === 0
224
- && unlimited && accountingAvailable && generationAvailable && !purchaseRequired && manageUrl === null;
225
- }
226
- if (state === "accounting_unavailable") {
227
- return balance === null && !balancePreview && charged === 0 && !unlimited && !accountingAvailable
228
- && !purchaseRequired && manageUrl === null && unitCost === (mode === "free" ? 0 : 1)
229
- && generationAvailable === (mode === "free");
230
- }
231
- if (mode !== "credits" || balance === null || balancePreview || unitCost !== 1 || unlimited || !accountingAvailable) {
232
- return false;
233
- }
234
- if (state === "balance_available") {
235
- return balance > 5 && generationAvailable && !purchaseRequired && manageUrl === null;
236
- }
237
- if (state === "low_balance") {
238
- return balance >= 1 && balance <= 5 && generationAvailable && !purchaseRequired && manageUrl !== null;
239
- }
240
- return state === "zero_balance" && balance === 0 && !generationAvailable && purchaseRequired && manageUrl !== null;
241
- }
242
-
243
- /**
244
- * Rebuild the public credit envelope from known primitives only. Never copy an
245
- * upstream message or unknown field into an MCP response.
246
- */
247
- export function safeCreditPresentation(value) {
248
- if (!plainObject(value)) return null;
249
- const balance = value.balance;
250
- const manageUrl = safeManageUrl(value.manageUrl);
251
- if (
252
- !CREDIT_STATES.has(value.state)
253
- || !CREDIT_MODES.has(value.mode)
254
- || !(balance === null || (Number.isSafeInteger(balance) && balance >= 0))
255
- || typeof value.balancePreview !== "boolean"
256
- || ![0, 1].includes(value.unitCost)
257
- || ![0, 1].includes(value.charged)
258
- || typeof value.unlimited !== "boolean"
259
- || typeof value.accountingAvailable !== "boolean"
260
- || typeof value.generationAvailable !== "boolean"
261
- || typeof value.purchaseRequired !== "boolean"
262
- || manageUrl === undefined
263
- || typeof value.replayed !== "boolean"
264
- ) return null;
265
-
266
- const credits = {
267
- state: value.state,
268
- mode: value.mode,
269
- balance,
270
- balancePreview: value.balancePreview,
271
- unitCost: value.unitCost,
272
- charged: value.charged,
273
- unlimited: value.unlimited,
274
- accountingAvailable: value.accountingAvailable,
275
- generationAvailable: value.generationAvailable,
276
- purchaseRequired: value.purchaseRequired,
277
- manageUrl,
278
- replayed: value.replayed,
279
- };
280
- if (!validCreditInvariant(credits)) return null;
281
- return { ...credits, message: creditMessage(credits) };
282
- }
283
-
284
- function safeCreditToolErrorPayload(value) {
285
- if (!plainObject(value) || !plainObject(value.error) || !plainObject(value.retry)) return null;
286
- const credits = safeCreditPresentation(value.credits);
287
- if (!credits) return null;
288
- const purchaseRequired = value.status === "purchase_required"
289
- && value.error.code === "insufficient_credits"
290
- && credits.state === "zero_balance"
291
- && credits.mode === "credits"
292
- && credits.balance === 0
293
- && credits.charged === 0
294
- && value.retry.after === "balance_updated";
295
- const accountingUnavailable = value.status === "accounting_unavailable"
296
- && value.error.code === "credit_accounting_unavailable"
297
- && credits.state === "accounting_unavailable"
298
- && credits.mode === "credits"
299
- && credits.charged === 0
300
- && value.retry.after === "accounting_available";
301
- if (
302
- (!purchaseRequired && !accountingUnavailable)
303
- || value.error.retryable !== true
304
- || value.retry.safe !== true
305
- || value.retry.idempotent !== true
306
- || value.retry.scope !== "pre_generation"
307
- ) return null;
308
-
309
- const message = purchaseRequired
310
- ? `This request needs 1 credit; the balance is 0. Nothing was charged. Credit management: ${credits.manageUrl}. The same request remains safe to retry after the balance changes.`
311
- : "The credit balance is temporarily unavailable. No generation started and no credit was used. The same request remains safe to retry after accounting recovers.";
312
- return {
313
- status: value.status,
314
- message,
315
- error: { code: value.error.code, retryable: true },
316
- credits: { ...credits, message },
317
- retry: {
318
- safe: true,
319
- idempotent: true,
320
- scope: "pre_generation",
321
- after: value.retry.after,
322
- },
323
- };
324
- }
325
-
326
158
  async function accountRequestError(res, toolName) {
327
159
  if ((res.headers.get("content-type") || "").includes("application/json")) {
328
160
  try {
329
161
  const payload = await res.json();
330
- const creditContent = safeCreditToolErrorPayload(payload);
331
- if (creditContent) {
332
- return new AccountRequestError(creditContent.message, res.status, creditContent);
333
- }
334
162
  const creationContent = toolName === "create_my_voice" ? safeCreationFailurePayload(payload) : null;
335
163
  if (creationContent) {
336
164
  return new AccountRequestError(creationContent.message, res.status, creationContent);
@@ -608,72 +436,6 @@ const PROVENANCE_OUTPUT_PROPERTIES = {
608
436
  voice: SELECTED_VOICE_SCHEMA,
609
437
  evidence: EVIDENCE_OUTPUT_SCHEMA,
610
438
  };
611
- const CREDIT_OUTPUT_SCHEMA = {
612
- type: "object",
613
- additionalProperties: false,
614
- properties: {
615
- state: {
616
- type: "string",
617
- enum: ["free_beta", "balance_available", "low_balance", "zero_balance", "legacy_unlimited", "accounting_unavailable"],
618
- },
619
- mode: { type: "string", enum: ["free", "credits"] },
620
- balance: { type: ["integer", "null"], minimum: 0 },
621
- balancePreview: { type: "boolean" },
622
- unitCost: { type: "number", enum: [0, 1] },
623
- charged: { type: "number", enum: [0, 1] },
624
- unlimited: { type: "boolean" },
625
- accountingAvailable: { type: "boolean" },
626
- generationAvailable: { type: "boolean" },
627
- purchaseRequired: { type: "boolean" },
628
- manageUrl: { type: ["string", "null"] },
629
- replayed: { type: "boolean" },
630
- message: { type: "string" },
631
- },
632
- required: [
633
- "state",
634
- "mode",
635
- "balance",
636
- "balancePreview",
637
- "unitCost",
638
- "charged",
639
- "unlimited",
640
- "accountingAvailable",
641
- "generationAvailable",
642
- "purchaseRequired",
643
- "manageUrl",
644
- "replayed",
645
- "message",
646
- ],
647
- };
648
- const CREDIT_ERROR_SCHEMA = {
649
- type: "object",
650
- additionalProperties: false,
651
- properties: {
652
- code: { type: "string", enum: ["insufficient_credits", "credit_accounting_unavailable"] },
653
- retryable: { type: "boolean", enum: [true] },
654
- },
655
- required: ["code", "retryable"],
656
- };
657
- const CREDIT_RETRY_SCHEMA = {
658
- type: "object",
659
- additionalProperties: false,
660
- properties: {
661
- safe: { type: "boolean", enum: [true] },
662
- idempotent: { type: "boolean", enum: [true] },
663
- scope: { type: "string", enum: ["pre_generation"] },
664
- after: { type: "string", enum: ["balance_updated", "accounting_available"] },
665
- },
666
- required: ["safe", "idempotent", "scope", "after"],
667
- };
668
- // Carried by a credit-gate refusal only; a completed draft or rewrite no longer reports
669
- // credits (13 constants in free mode) or a status beside `state` (IDI-381).
670
- const CREDIT_GENERATION_OUTPUT_PROPERTIES = {
671
- status: { type: "string", enum: ["purchase_required", "accounting_unavailable"] },
672
- message: { type: "string" },
673
- credits: CREDIT_OUTPUT_SCHEMA,
674
- error: CREDIT_ERROR_SCHEMA,
675
- retry: CREDIT_RETRY_SCHEMA,
676
- };
677
439
  const NEXT_ACTION_SCHEMA = {
678
440
  type: "object",
679
441
  additionalProperties: false,
@@ -735,7 +497,6 @@ const REWRITE_OUTPUT_SCHEMA = {
735
497
  reason: { type: ["string", "null"], description: "Present only when scoring is enabled and the engine did not measure; says why." },
736
498
  voice: SELECTED_VOICE_SCHEMA,
737
499
  evidence: EVIDENCE_OUTPUT_SCHEMA,
738
- ...CREDIT_GENERATION_OUTPUT_PROPERTIES,
739
500
  ...CAVEATS_OUTPUT_PROPERTY,
740
501
  },
741
502
  };
@@ -747,7 +508,6 @@ const DRAFT_OUTPUT_SCHEMA = {
747
508
  reason: { type: ["string", "null"], description: "Present only when scoring is enabled and the engine did not measure; says why." },
748
509
  voice: SELECTED_VOICE_SCHEMA,
749
510
  evidence: EVIDENCE_OUTPUT_SCHEMA,
750
- ...CREDIT_GENERATION_OUTPUT_PROPERTIES,
751
511
  ...CAVEATS_OUTPUT_PROPERTY,
752
512
  },
753
513
  };
@@ -1012,20 +772,6 @@ const accountToolDefinitions = [
1012
772
  ]),
1013
773
  annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true, title: "Delete Writing Profile" },
1014
774
  },
1015
- {
1016
- name: "get_credit_status",
1017
- description:
1018
- "Check the account's current generation-credit state. During the free beta, this confirms that no credits are being used.",
1019
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
1020
- outputSchema: CREDIT_OUTPUT_SCHEMA,
1021
- annotations: {
1022
- readOnlyHint: true,
1023
- idempotentHint: true,
1024
- openWorldHint: false,
1025
- destructiveHint: false,
1026
- title: "Check Credit Status",
1027
- },
1028
- },
1029
775
  {
1030
776
  name: "score_voice",
1031
777
  description:
@@ -1061,6 +807,12 @@ const accountToolDefinitions = [
1061
807
  description:
1062
808
  "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.",
1063
809
  },
810
+ language: {
811
+ type: "string",
812
+ minLength: 1,
813
+ maxLength: 100,
814
+ 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.",
815
+ },
1064
816
  changeScope: CHANGE_SCOPE_SCHEMA,
1065
817
  effects: EFFECTS_SCHEMA,
1066
818
  sourceAuthor: SOURCE_AUTHOR_SCHEMA,
@@ -1101,6 +853,12 @@ const accountToolDefinitions = [
1101
853
  description:
1102
854
  "`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`.",
1103
855
  },
856
+ language: {
857
+ type: "string",
858
+ minLength: 1,
859
+ maxLength: 100,
860
+ 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.",
861
+ },
1104
862
  effects: EFFECTS_SCHEMA,
1105
863
  briefAuthor: BRIEF_AUTHOR_SCHEMA,
1106
864
  calibration: CALIBRATION_SCHEMA,
@@ -1468,18 +1226,7 @@ export async function handleAccountTool(name, args = {}) {
1468
1226
  }
1469
1227
  }
1470
1228
  if (name === "get_credit_status") {
1471
- try {
1472
- const credits = safeCreditPresentation(await accountRequest("/api/v1/credits", {
1473
- toolName: "get_credit_status",
1474
- }));
1475
- if (!credits) return toolError("get_credit_status: Idiolect AI returned an invalid credit status.");
1476
- return {
1477
- content: [{ type: "text", text: credits.message }],
1478
- structuredContent: credits,
1479
- };
1480
- } catch (error) {
1481
- return accountToolError(error);
1482
- }
1229
+ return toolError("This tool is no longer available. Refresh the Idiolect AI tool catalogue.");
1483
1230
  }
1484
1231
 
1485
1232
  if (name === "get_my_voice") {
@@ -1625,6 +1372,9 @@ export async function handleToolRequest(req) {
1625
1372
  const instructions = typeof args.instructions === "string" && args.instructions.trim()
1626
1373
  ? args.instructions.trim()
1627
1374
  : undefined;
1375
+ const language = typeof args.language === "string" && args.language.trim()
1376
+ ? args.language.trim()
1377
+ : undefined;
1628
1378
  const invalidChangeRequest = changeRequestError(args, "rewrite_in_voice");
1629
1379
  if (invalidChangeRequest) return invalidChangeRequest;
1630
1380
  const invalidSourceAuthor = authorProvenanceError(args, "rewrite_in_voice", "sourceAuthor");
@@ -1655,6 +1405,7 @@ export async function handleToolRequest(req) {
1655
1405
  ...(profileId !== undefined ? { voice: profileId } : {}),
1656
1406
  ...(targetRegister ? { targetRegister } : {}),
1657
1407
  ...(instructions ? { instructions } : {}),
1408
+ ...(language ? { language } : {}),
1658
1409
  ...(changeScope !== undefined ? { changeScope } : {}),
1659
1410
  ...(effects !== undefined ? { effects } : {}),
1660
1411
  ...(sourceAuthor !== undefined ? { sourceAuthor } : {}),
@@ -1686,6 +1437,9 @@ export async function handleToolRequest(req) {
1686
1437
  const instructions = typeof args.instructions === "string" && args.instructions.trim()
1687
1438
  ? args.instructions.trim()
1688
1439
  : undefined;
1440
+ const language = typeof args.language === "string" && args.language.trim()
1441
+ ? args.language.trim()
1442
+ : undefined;
1689
1443
  const formats = new Set(["linkedin", "x_post", "x_thread", "newsletter", "generic"]);
1690
1444
  const invalidChangeRequest = changeRequestError(args, "draft_in_voice");
1691
1445
  if (invalidChangeRequest) return invalidChangeRequest;
@@ -1717,6 +1471,7 @@ export async function handleToolRequest(req) {
1717
1471
  ...(profileId !== undefined ? { voice: profileId } : {}),
1718
1472
  ...(format ? { format } : {}),
1719
1473
  ...(instructions ? { instructions } : {}),
1474
+ ...(language ? { language } : {}),
1720
1475
  ...(changeScope !== undefined ? { changeScope } : {}),
1721
1476
  ...(effects !== undefined ? { effects } : {}),
1722
1477
  ...(briefAuthor !== undefined ? { briefAuthor } : {}),