@nooticr/mcp 1.26.24 → 1.26.25

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.
@@ -18,6 +18,49 @@
18
18
  import { z } from "zod";
19
19
  /** Loose by construction: see the note above. */
20
20
  const open = (shape) => z.object(shape).passthrough();
21
+ /**
22
+ * A union branch that renders as its own `type`, instead of being folded into
23
+ * a `type` array with its siblings.
24
+ *
25
+ * `z.union([z.string(), z.number(), z.boolean()])` renders as
26
+ * `{ type: ["string", "number", "boolean"] }`. That is legal JSON Schema and
27
+ * the wrong thing to send: a client that reads `type` as a single string
28
+ * either rejects the tool or drops the constraint, and both failures are
29
+ * invisible from here — a rejected tool just stops appearing in `tools/list`,
30
+ * with no error this repo would ever see. The whole surface emitted 1,286 of
31
+ * them, from this helper and `listOf` alone.
32
+ *
33
+ * The fold happens in zod-to-json-schema's union parser, which collapses a
34
+ * union whose every member is a bare primitive carrying no checks. There are
35
+ * only two ways out, and only one of them is free:
36
+ *
37
+ * - Give a member a check. `z.number().finite()` renders as plain
38
+ * `{ type: "number" }` and defeats the fold, but it also stops accepting
39
+ * `Infinity` — which a ratio-to-baseline division by a zero median really
40
+ * does produce, and which this file exists to keep from turning a working
41
+ * call into a hard failure. Rejected for that reason, not on taste.
42
+ * - Give a member a wrapper whose type name is not a primitive.
43
+ * `.readonly()` is the one that costs nothing: on a primitive it is a
44
+ * runtime no-op (`Object.freeze` of a string is that string), the parsed
45
+ * value is discarded by the SDK's output validation anyway, and
46
+ * `Readonly<string>` is `string`, so nothing downstream sees a new type.
47
+ * It also happens to be true — these are output schemas.
48
+ *
49
+ * So the wrapper is load-bearing and is not about readonly-ness. Removing it
50
+ * silently reintroduces all 1,286, which is what
51
+ * `tests/output-schema-shape.test.ts` is there to catch.
52
+ */
53
+ const oneType = (branch) => branch.readonly();
54
+ /**
55
+ * "This type, or null" — as `anyOf`, not as a two-element `type` array.
56
+ *
57
+ * `z.number().nullish()` renders as `{ type: ["number", "null"] }`, which is
58
+ * the same portability problem as `scalar()` in miniature. The three inline
59
+ * output schemas in `watchlist.ts` contributed 5 of the 1,286 this way, and
60
+ * they are the reason this is exported rather than kept private: a tool that
61
+ * declares its schema inline should not have to re-derive why.
62
+ */
63
+ export const orNull = (inner) => z.union([oneType(inner), z.null()]).optional();
21
64
  /**
22
65
  * One scalar type for every leaf, and it accepts null.
23
66
  *
@@ -33,10 +76,15 @@ const open = (shape) => z.object(shape).passthrough();
33
76
  * agent needs from this file is which keys exist and what they mean. So the
34
77
  * keys and their descriptions are the contract, and the types are deliberately
35
78
  * as wide as the data can be.
79
+ *
80
+ * Which is why the branches are wrapped in `oneType()` and null is a member of
81
+ * the union rather than a `.nullish()` around it. Both exist to control the
82
+ * JSON Schema this renders as, and neither changes what the schema accepts —
83
+ * see `oneType` below.
36
84
  */
37
- const scalar = () => z.union([z.string(), z.number(), z.boolean()]).nullish();
85
+ const scalar = () => z.union([oneType(z.string()), oneType(z.number()), oneType(z.boolean()), z.null()]).optional();
38
86
  /** Same reasoning for lists: a null list, and a null element, must both pass. */
39
- const listOf = (item) => z.array(z.union([item, z.null()])).nullish();
87
+ const listOf = (item) => z.array(z.union([oneType(item), z.null()])).nullish();
40
88
  /**
41
89
  * A list whose elements are not one shape — themes, fixes, failures.
42
90
  *
@@ -49,6 +97,23 @@ const listOf = (item) => z.array(z.union([item, z.null()])).nullish();
49
97
  * empty schema in item position, which accepts anything, which is the point.
50
98
  */
51
99
  const anyList = () => z.array(z.any()).nullish();
100
+ /**
101
+ * "An object whose contents we are not constraining" — for input schemas.
102
+ *
103
+ * Exported from a file about output schemas because the concern is the same
104
+ * one documented above: what a zod type renders as on the wire. The natural
105
+ * spelling, `z.record(z.unknown())`, renders as
106
+ * `{ type: "object", additionalProperties: {} }`, and that `{}` carries no
107
+ * validation keyword at all — the object-literal spelling of a bare `true`,
108
+ * which reads to a strict client as an unvalidated schema rather than a
109
+ * deliberately open one. `z.object({}).passthrough()` accepts exactly the same
110
+ * values and says `additionalProperties: true`, which is the same permission
111
+ * stated in a keyword rather than by omission.
112
+ *
113
+ * The array counterpart needs no helper: `z.array(z.unknown())` emits
114
+ * `items: {}` for the same reason, and `z.array(z.any())` simply omits `items`.
115
+ */
116
+ export const anyObject = () => z.object({}).passthrough();
52
117
  /** Every paid tool reports what it charged. */
53
118
  const mcpCredits = open({
54
119
  cost: scalar().describe("Credits this call consumed."),
@@ -411,7 +411,7 @@ export function registerOwnAccountTools(server, makeClient) {
411
411
  caption: z.string().optional(),
412
412
  contentType: z.string().optional(),
413
413
  thumbnailUrl: z.string().optional().describe("Looked at by the reviewer when given."),
414
- mediaItems: z.array(z.unknown()).optional(),
414
+ mediaItems: z.array(z.any()).optional(),
415
415
  platform: z.string().optional(),
416
416
  influencerId: z.number().int().optional(),
417
417
  })
@@ -160,13 +160,13 @@ export const TOOL_DEFINITIONS = [
160
160
  name: "show_comparison",
161
161
  title: "Show Comparison",
162
162
  description: "Display a comparison you wrote after compare_posts fetched the first post and you fetched the rest yourself. Free, and makes no requests — it only draws what you pass it: each post with a BEST badge on the winner, what differed, shared strengths and the next experiment worth running. Call this after you have done the comparing, not instead of it.",
163
- inputSchema: z.object({ posts: z.array(z.record(z.unknown())).min(2).max(5), winner: z.number().int(), winnerReason: z.string().optional(), differences: z.array(z.object({ factor: z.string(), detail: z.string() })).optional(), lessons: z.array(z.string()).optional(), nextTest: z.string().optional() }).strict(),
163
+ inputSchema: z.object({ posts: z.array(z.object({}).passthrough()).min(2).max(5), winner: z.number().int(), winnerReason: z.string().optional(), differences: z.array(z.object({ factor: z.string(), detail: z.string() })).optional(), lessons: z.array(z.string()).optional(), nextTest: z.string().optional() }).strict(),
164
164
  },
165
165
  {
166
166
  name: "show_analysis",
167
167
  title: "Show Analysis",
168
168
  description: "Display an analysis you wrote after analyze_post, analyze_post_fast or understand_social_post handed you the material. Free, and makes no requests — it only draws what you pass it: hook strength, script structure, quotable lines, hashtags, target audience, viral triggers and more, whichever of these you actually produced. Call this after you have done the analysing, not instead of it.",
169
- inputSchema: z.object({ url: z.string(), post: z.record(z.unknown()).optional(), analysis: z.record(z.unknown()) }).strict(),
169
+ inputSchema: z.object({ url: z.string(), post: z.object({}).passthrough().optional(), analysis: z.object({}).passthrough() }).strict(),
170
170
  },
171
171
  {
172
172
  name: "show_hooks",
@@ -178,7 +178,7 @@ export const TOOL_DEFINITIONS = [
178
178
  name: "show_variants",
179
179
  title: "Show Variants",
180
180
  description: "Display the post variants you wrote after create_variants handed you the original post's material. Free, and makes no requests — it only draws what you pass it: each variant's hook, the angle that changes, its shot beats and its call to action. Call this after you have written the variants, not instead of writing them.",
181
- inputSchema: z.object({ sourceUrl: z.string(), post: z.record(z.unknown()).optional(), variants: z.array(z.object({ title: z.string(), hook: z.string(), angle: z.string().optional(), beats: z.array(z.string()).optional(), cta: z.string().optional(), whyItCouldWork: z.string().optional() })).min(1) }).strict(),
181
+ inputSchema: z.object({ sourceUrl: z.string(), post: z.object({}).passthrough().optional(), variants: z.array(z.object({ title: z.string(), hook: z.string(), angle: z.string().optional(), beats: z.array(z.string()).optional(), cta: z.string().optional(), whyItCouldWork: z.string().optional() })).min(1) }).strict(),
182
182
  },
183
183
  {
184
184
  name: "show_repurposed_post",
@@ -292,7 +292,7 @@ export const TOOL_DEFINITIONS = [
292
292
  name: "review_post",
293
293
  title: "Review Post",
294
294
  description: "Score a post before you publish it: hook strength, an optional A-vs-B hook comparison, aesthetic and storytelling notes, and rewritten hook/caption suggestions. Pass postId for something already in your pipeline, or appId plus draft fields otherwise. Nothing is published. Free — nothing is billed for this, same as the dashboard's own pre-publish review.",
295
- inputSchema: z.object({ postId: z.number().int().optional(), appId: z.number().int().optional(), title: z.string().optional(), titleB: z.string().optional(), caption: z.string().optional(), contentType: z.string().optional(), thumbnailUrl: z.string().optional(), mediaItems: z.array(z.unknown()).optional(), platform: z.string().optional(), influencerId: z.number().int().optional() }).strict(),
295
+ inputSchema: z.object({ postId: z.number().int().optional(), appId: z.number().int().optional(), title: z.string().optional(), titleB: z.string().optional(), caption: z.string().optional(), contentType: z.string().optional(), thumbnailUrl: z.string().optional(), mediaItems: z.array(z.any()).optional(), platform: z.string().optional(), influencerId: z.number().int().optional() }).strict(),
296
296
  },
297
297
  {
298
298
  name: "draft_post",
@@ -10,7 +10,7 @@ import { z } from "zod";
10
10
  import { NooticrError } from "./nooticr.js";
11
11
  import { NOOTICR_UI_TEMPLATE } from "./ui-template.js";
12
12
  import { registerPrompts } from "./prompts.js";
13
- import { OUTPUT_SCHEMAS } from "./output-schemas.js";
13
+ import { OUTPUT_SCHEMAS, anyObject } from "./output-schemas.js";
14
14
  import { createTaskStore, registerSlowTool } from "./tasks.js";
15
15
  import { COMMENT_CATEGORIES, COMMENT_SENTIMENTS, platformFromUrl, reviewGuidance, toEvidence, } from "./comment-review.js";
16
16
  import { costSentence, EVIDENCE_PLANS, fetchBillingNote, frameIndex, framesToBlocks, planCalls, scoreDraftGuidance, } from "./evidence.js";
@@ -24,7 +24,7 @@ import { loadingPlansJson } from "./loading-plans.js";
24
24
  import { registerHandoff } from "./handoff.js";
25
25
  import { registerCollabTools } from "./collab.js";
26
26
  /** Current MCP server version — bumped on every deploy for traceability. */
27
- export const MCP_SERVER_VERSION = "1.26.24";
27
+ export const MCP_SERVER_VERSION = "1.26.25";
28
28
  /** MCP Apps extension identifier */
29
29
  const UI_EXTENSION = "io.modelcontextprotocol/ui";
30
30
  /** MIME type for MCP Apps HTML resources */
@@ -1350,7 +1350,7 @@ export function createMcpServer(rawMakeClient, opts) {
1350
1350
  inputSchema: z
1351
1351
  .object({
1352
1352
  posts: z
1353
- .array(z.record(z.unknown()))
1353
+ .array(anyObject())
1354
1354
  .min(2)
1355
1355
  .max(5)
1356
1356
  .describe("The 2-5 posts compared, in the order you compared them — the same shape get_social_media " +
@@ -1403,12 +1403,10 @@ export function createMcpServer(rawMakeClient, opts) {
1403
1403
  inputSchema: z
1404
1404
  .object({
1405
1405
  url: z.string().describe("The post you analyzed."),
1406
- post: z
1407
- .record(z.unknown())
1406
+ post: anyObject()
1408
1407
  .optional()
1409
1408
  .describe("The post object analyze_post/analyze_post_fast/understand_social_post handed you, unchanged."),
1410
- analysis: z
1411
- .record(z.unknown())
1409
+ analysis: anyObject()
1412
1410
  .describe("Your own analysis. Any of: summary, hookStrength (1-10), commentBaitLevel (1-10), " +
1413
1411
  "scriptStructure {hook,buildUp,payoff,cta}, whyItWorks, suggestedHook, keyQuotes[], " +
1414
1412
  "suggestedHashtags[], targetAudience, viralTriggers[], negativeSignals[], variationIdeas[], " +
@@ -1480,7 +1478,7 @@ export function createMcpServer(rawMakeClient, opts) {
1480
1478
  inputSchema: z
1481
1479
  .object({
1482
1480
  sourceUrl: z.string().describe("The post these variants riff on."),
1483
- post: z.record(z.unknown()).optional().describe("The post object create_variants handed you, unchanged."),
1481
+ post: anyObject().optional().describe("The post object create_variants handed you, unchanged."),
1484
1482
  variants: z
1485
1483
  .array(z.object({
1486
1484
  title: z.string().describe("A short label for this variant."),
@@ -46,6 +46,7 @@
46
46
  import { z } from "zod";
47
47
  import { confirmSpend, declinedResult, CREDITS_PER_CREATOR } from "./spend.js";
48
48
  import { PLATFORM_ARG } from "./evidence.js";
49
+ import { orNull } from "./output-schemas.js";
49
50
  export const WATCHLIST_URI = "nooticr://watchlist";
50
51
  /** Default, and what the tests use. Fine for a single process. */
51
52
  export class MemoryWatchStore {
@@ -455,8 +456,8 @@ export function registerWatchlist(server, makeClient, store) {
455
456
  .strict(),
456
457
  outputSchema: z
457
458
  .object({
458
- watching: z.number().nullish(),
459
- added: z.string().nullish(),
459
+ watching: orNull(z.number()),
460
+ added: orNull(z.string()),
460
461
  entries: z.array(z.union([z.object({}).passthrough(), z.null()])).nullish(),
461
462
  })
462
463
  .passthrough(),
@@ -491,8 +492,8 @@ export function registerWatchlist(server, makeClient, store) {
491
492
  .strict(),
492
493
  outputSchema: z
493
494
  .object({
494
- removed: z.boolean().nullish(),
495
- watching: z.number().nullish(),
495
+ removed: orNull(z.boolean()),
496
+ watching: orNull(z.number()),
496
497
  entries: z.array(z.union([z.object({}).passthrough(), z.null()])).nullish(),
497
498
  })
498
499
  .passthrough(),
@@ -525,7 +526,7 @@ export function registerWatchlist(server, makeClient, store) {
525
526
  .strict(),
526
527
  outputSchema: z
527
528
  .object({
528
- checked: z.number().nullish(),
529
+ checked: orNull(z.number()),
529
530
  creators: z.array(z.union([z.object({}).passthrough(), z.null()])).nullish(),
530
531
  posts: z.array(z.union([z.object({}).passthrough(), z.null()])).nullish().describe("Everything new, flattened, for the card view."),
531
532
  mcpCredits: z.object({}).passthrough().nullish(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nooticr/mcp",
3
3
  "mcpName": "com.nooticr/mcp",
4
- "version": "1.26.24",
4
+ "version": "1.26.25",
5
5
  "description": "MCP server for nooticr - fetch, discover and understand TikTok/Instagram/YouTube/X/Reddit/LinkedIn/Weibo posts with AI (media metadata, niche discovery, brand monitoring, hook & viral analysis)",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",