@orchyn/mcp 1.13.0 → 1.14.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.
@@ -11,13 +11,36 @@ import { OrchynError } from "./orchyn.js";
11
11
  import { formatPaywallError, runVideoAnalysis, validatePostUrl } from "./video.js";
12
12
  import { ORCHYN_UI_TEMPLATE } from "./ui-template.js";
13
13
  /** Current MCP server version — bumped on every deploy for traceability. */
14
- export const MCP_SERVER_VERSION = "1.13.0";
14
+ export const MCP_SERVER_VERSION = "1.14.0";
15
15
  /** MCP Apps extension identifier */
16
16
  const UI_EXTENSION = "io.modelcontextprotocol/ui";
17
17
  /** MIME type for MCP Apps HTML resources */
18
18
  const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
19
19
  /** Single UI resource URI shared by all tools */
20
20
  const UI_RESOURCE_URI = "ui://orchyn/view";
21
+ /**
22
+ * claude.ai requires `ui.domain` on the resource == sha256("<MCP endpoint
23
+ * URL>")[:32] + ".claudemcpcontent.com" — the iframe is only revealed on
24
+ * that dedicated sandbox origin. The endpoint is the worker's public origin
25
+ * + "/mcp". stdio runs have no public URL, so the field is omitted and the
26
+ * host falls back to its default per-conversation origin.
27
+ */
28
+ async function computeAppDomain() {
29
+ const publicUrl = (process.env.PUBLIC_URL || "").trim().replace(/\/+$/, "");
30
+ if (!publicUrl)
31
+ return undefined;
32
+ try {
33
+ const endpoint = `${publicUrl}/mcp`;
34
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(endpoint));
35
+ const hex = Array.from(new Uint8Array(digest))
36
+ .map((b) => b.toString(16).padStart(2, "0"))
37
+ .join("");
38
+ return `${hex.slice(0, 32)}.claudemcpcontent.com`;
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
21
44
  /** Convert an arbitrary http(s) URL to a base64 MCP `image` data block. */
22
45
  async function fetchAsBase64Image(url, mimeType) {
23
46
  try {
@@ -87,9 +110,11 @@ async function toToolResult(proxy) {
87
110
  // JPEG thumbnails (HEIC already transcoded), so convert those URLs to
88
111
  // base64 data here — guaranteeing clients render a valid image.
89
112
  const imgBlocks = proxy.contentBlocks.filter((c) => c.type === "image");
90
- const images = await Promise.all(imgBlocks.map((c) => c.url
113
+ // Skip image blocks whose base64 is empty (fetch failed) clients reject
114
+ // them with "could not be processed: Error processing image".
115
+ const images = (await Promise.all(imgBlocks.map((c) => c.url
91
116
  ? fetchAsBase64Image(String(c.url), String(c.mimeType ?? "image/jpeg"))
92
- : Promise.resolve({ type: "image", data: String(c.data ?? ""), mimeType: String(c.mimeType ?? "image/jpeg") })));
117
+ : Promise.resolve({ type: "image", data: String(c.data ?? ""), mimeType: String(c.mimeType ?? "image/jpeg") })))).filter((img) => img.type === "image" && img.data.length > 0);
93
118
  // The Rust backend embeds HTML cards directly in the text block
94
119
  // (type:"text", text:"<div>...</div>\n\n{json}"). We extract the HTML
95
120
  // prefix from the first text contentBlock and prepend it.
@@ -251,6 +276,7 @@ export function createMcpServer(makeClient) {
251
276
  if (apiUrl && apiUrl.trim()) {
252
277
  domains.push(apiUrl.trim().replace(/\/+$/, ""));
253
278
  }
279
+ const domain = await computeAppDomain();
254
280
  return {
255
281
  contents: [
256
282
  {
@@ -259,9 +285,11 @@ export function createMcpServer(makeClient) {
259
285
  text: ORCHYN_UI_TEMPLATE,
260
286
  _meta: {
261
287
  ui: {
288
+ ...(domain ? { domain } : {}),
262
289
  csp: {
263
290
  resourceDomains: domains,
264
291
  },
292
+ prefersBorder: false,
265
293
  },
266
294
  },
267
295
  },
@@ -273,7 +301,7 @@ export function createMcpServer(makeClient) {
273
301
  description: "Analyze a social post (video, image, carousel/slideshow) from its link — " +
274
302
  "imports the media and runs AI analysis over the actual content (video frames, carousel images, caption). " +
275
303
  "Supports TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu and Bilibili. Returns the full analysis once finished. First use free per user.",
276
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
304
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
277
305
  inputSchema: z
278
306
  .object({
279
307
  url: z.string().describe("Public post URL (TikTok/Instagram/YouTube/X, Douyin, Xiaohongshu or Bilibili)."),
@@ -305,11 +333,13 @@ export function createMcpServer(makeClient) {
305
333
  // The backend attaches `_inlineImages` as permanent orchyn public URLs
306
334
  // (re-hosted into storage, HEIC already transcoded to JPEG). The SDK's
307
335
  // ImageContent only accepts base64 `data`, so fetch each URL and encode it.
308
- const images = await Promise.all((result.inlineImages ?? [])
336
+ // Skip image blocks whose base64 is empty (fetch failed) — clients
337
+ // reject them with "could not be processed: Error processing image".
338
+ const images = (await Promise.all((result.inlineImages ?? [])
309
339
  .filter((img) => typeof img === "object" && img !== null)
310
340
  .map((img) => "url" in img && img.url
311
341
  ? fetchAsBase64Image(String(img.url), String(img.mimeType ?? "image/jpeg"))
312
- : Promise.resolve({ type: "image", data: String(img.data ?? ""), mimeType: String(img.mimeType ?? "image/jpeg") })));
342
+ : Promise.resolve({ type: "image", data: String(img.data ?? ""), mimeType: String(img.mimeType ?? "image/jpeg") })))).filter((img) => img.type === "image" && img.data.length > 0);
313
343
  const textJson = JSON.stringify(proxied, null, 2);
314
344
  return {
315
345
  content: [...images, { type: "text", text: `${htmlCard}\n\n${textJson}` }],
@@ -332,7 +362,7 @@ export function createMcpServer(makeClient) {
332
362
  description: "Fetch a social post's media from a TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu or Bilibili URL: " +
333
363
  "contentType (video/image/carousel/slideshow), title, caption, author, stats and direct media URLs. " +
334
364
  "Returns an inline thumbnail image. Consumes 1 orchyn credit. First use free per user.",
335
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
365
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
336
366
  inputSchema: z
337
367
  .object({
338
368
  url: z.string().describe("Full public post URL."),
@@ -352,7 +382,7 @@ export function createMcpServer(makeClient) {
352
382
  description: "Discover recent posts (video, image, carousel, slideshow) for a niche on YouTube, TikTok, Instagram, Douyin, Xiaohongshu, X/Twitter or Bilibili. " +
353
383
  "Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
354
384
  'Say "next" to paginate (offset), or "analyze the 2nd one" / "analyze all" for batch analysis. Consumes 2 orchyn credits. First use free per user.',
355
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
385
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
356
386
  inputSchema: z
357
387
  .object({
358
388
  niche: z.string().describe("Niche/topic, e.g. 'fitness'."),
@@ -379,7 +409,7 @@ export function createMcpServer(makeClient) {
379
409
  description: "List recent posts by a creator handle (e.g. @zoundsapp) on TikTok, Instagram, YouTube, Douyin, Xiaohongshu, X/Twitter, Bilibili or LinkedIn (LinkedIn uses the profile public_id, e.g. 'billgates'). " +
380
410
  "Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
381
411
  "Use this when Claude needs to pull more posts from the same account to spot a pattern, or to scan a whole profile. Consumes 2 orchyn credits. First use free per user.",
382
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
412
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
383
413
  inputSchema: z
384
414
  .object({
385
415
  username: z.string().describe("Creator handle, e.g. 'zoundsapp' or '@zoundsapp'."),
@@ -404,7 +434,7 @@ export function createMcpServer(makeClient) {
404
434
  description: "Deep-dive a whole creator profile on TikTok, Instagram, YouTube, Douyin, Xiaohongshu, X/Twitter, Bilibili or LinkedIn: fetch recent posts, run multimodal AI on up to 3, " +
405
435
  "then synthesize a profile report — creator summary, niche, content themes, hook styles, strengths/weaknesses, " +
406
436
  "engagement patterns, audience insights, variation ideas, collaboration fit. Consumes 15 orchyn credits. First use free per user.",
407
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
437
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
408
438
  inputSchema: z
409
439
  .object({
410
440
  username: z.string().describe("Creator handle, e.g. 'zoundsapp'."),
@@ -429,7 +459,7 @@ export function createMcpServer(makeClient) {
429
459
  title: "Get Post Comments",
430
460
  description: "Fetch top comments for a post URL on TikTok, Instagram, YouTube, Douyin, X/Twitter, Bilibili or LinkedIn, plus keyword clusters from TikTok Analytics " +
431
461
  "when available — audience sentiment/audience-signal analysis. Consumes 2 orchyn credits. First use free per user.",
432
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
462
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
433
463
  inputSchema: z
434
464
  .object({
435
465
  url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/Douyin/X/Bilibili/LinkedIn)."),
@@ -449,7 +479,7 @@ export function createMcpServer(makeClient) {
449
479
  title: "Search Creators",
450
480
  description: "Search creators by niche/keyword on TikTok, Instagram, Xiaohongshu, YouTube or Douyin — username, nickname, follower count, " +
451
481
  "signature, verified status. Use to find influencers to vet or analyze. Consumes 2 orchyn credits. First use free per user.",
452
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
482
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
453
483
  inputSchema: z
454
484
  .object({
455
485
  keyword: z.string().describe("Niche/keyword, e.g. 'fitness' or a creator name."),
@@ -473,7 +503,7 @@ export function createMcpServer(makeClient) {
473
503
  title: "Get Similar Creators",
474
504
  description: "Find lookalike creators for a given handle — TikTok similar-user recommendations or Instagram " +
475
505
  "similar users. Useful for scaling: 'if this creator works, here are more like them'. Consumes 2 orchyn credits. First use free per user.",
476
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
506
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
477
507
  inputSchema: z
478
508
  .object({
479
509
  username: z.string().describe("Seed creator handle, e.g. 'zoundsapp'."),
@@ -493,7 +523,7 @@ export function createMcpServer(makeClient) {
493
523
  title: "Discover Sounds",
494
524
  description: "Discover trending sounds/music for a keyword on TikTok or Instagram — the sound is a huge ranking " +
495
525
  "signal for TikTok virality. Returns title, artist, duration, play/cover URLs. Consumes 2 orchyn credits. First use free per user.",
496
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
526
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
497
527
  inputSchema: z
498
528
  .object({
499
529
  keyword: z.string().describe("Niche/keyword, e.g. 'gym'."),
@@ -513,7 +543,7 @@ export function createMcpServer(makeClient) {
513
543
  server.registerTool("check_orchyn_credits", {
514
544
  title: "Check Orchyn Credits",
515
545
  description: "Check your orchyn credit balance, billing URL and pack size. No cost — call anytime to see remaining credits before running other tools.",
516
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
546
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
517
547
  inputSchema: z.object({}).strict(),
518
548
  }, async (_args, extra) => {
519
549
  const client = await makeClient(extra);
@@ -527,7 +557,7 @@ export function createMcpServer(makeClient) {
527
557
  server.registerTool("buy_orchyn_credits", {
528
558
  title: "Buy Orchyn Credits",
529
559
  description: "Buy an MCP credit pack via Stripe Checkout. Returns a secure checkout URL — open it in your browser to pay. Credits are added automatically after payment. No cost to call.",
530
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
560
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
531
561
  inputSchema: z.object({}).strict(),
532
562
  }, async (_args, extra) => {
533
563
  const client = await makeClient(extra);
@@ -543,7 +573,7 @@ export function createMcpServer(makeClient) {
543
573
  description: "Import a social post URL AND understand it with multimodal AI over the actual video/images: " +
544
574
  "summary, hook strength, viral triggers, format breakdown and variation ideas. Includes the thumbnail. " +
545
575
  "Supports TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu and Bilibili. Consumes 6 orchyn credits. First use free per user.",
546
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
576
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
547
577
  inputSchema: z
548
578
  .object({
549
579
  url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/X/Douyin/Xiaohongshu/Bilibili)."),
@@ -214,6 +214,7 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
214
214
  clearTimeout(loadingTimer);
215
215
  clearTimeout(fallbackTimer);
216
216
  render(d.params);
217
+ setTimeout(reportSize,50);
217
218
  }
218
219
  if(d.method==="ui/notifications/tool-input-partial"){
219
220
  currentTool=d.params&&d.params.name?d.params.name:"";
@@ -269,14 +270,40 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
269
270
  +'</div></div>';
270
271
  },3000);
271
272
 
272
- // MCP Apps handshake
273
+ // MCP Apps handshake.
274
+ // Claude's McpUiInitializeRequestSchema requires appInfo, appCapabilities
275
+ // and protocolVersion (legacy hosts read capabilities/clientInfo), so send
276
+ // both shapes. Critically, ui/notifications/initialized must be sent
277
+ // UNCONDITIONALLY — Claude keeps the widget iframe reserved-but-hidden
278
+ // until it receives that notification, so a reply we don't recognize must
279
+ // never deadlock the handshake.
280
+ var initializedSent=false;
281
+ function sendInitialized(){
282
+ if(initializedSent)return;
283
+ initializedSent=true;
284
+ window.parent.postMessage({jsonrpc:"2.0",method:"ui/notifications/initialized",params:{}},"*");
285
+ }
273
286
  send("ui/initialize",{
274
287
  protocolVersion:"2026-01-26",
288
+ appInfo:{name:"orchyn-view",version:"2.0.0"},
289
+ appCapabilities:{availableDisplayModes:["inline"]},
275
290
  capabilities:{},
276
291
  clientInfo:{name:"orchyn-view",version:"2.0.0"}
277
- }).then(function(){
278
- window.parent.postMessage({jsonrpc:"2.0",method:"ui/notifications/initialized",params:{}},"*");
279
- }).catch(function(){});
292
+ }).then(sendInitialized).catch(sendInitialized);
293
+ // Unconditional fallback: never wait on a reply shape we don't recognize.
294
+ setTimeout(sendInitialized, 500);
295
+
296
+ // Report content size so flexible hosts (Claude) size the iframe correctly.
297
+ // Params MUST be real numbers — claude.ai throws on null/missing width.
298
+ function reportSize(){
299
+ try{
300
+ var h=document.documentElement.scrollHeight||document.body.scrollHeight||400;
301
+ var w=document.documentElement.scrollWidth||document.body.scrollWidth||400;
302
+ window.parent.postMessage({jsonrpc:"2.0",method:"ui/notifications/size-changed",params:{height:h,width:w}},"*");
303
+ }catch(e){}
304
+ }
305
+ window.addEventListener("resize",reportSize);
306
+ setTimeout(reportSize,600);
280
307
 
281
308
  // ─── Helpers ───
282
309
  function esc(s){return String(s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}
@@ -563,6 +590,7 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
563
590
  if(d.post||d.platform){app.innerHTML=postCard(d.post||d,true);return;}
564
591
  // Fallback
565
592
  app.innerHTML='<div class="json-block fade-in">'+esc(JSON.stringify(d,null,2))+"</div>";
593
+ setTimeout(reportSize,50);
566
594
  }
567
595
 
568
596
  function updateProgress(params){
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchyn/mcp",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "MCP server for orchyn - fetch, discover and understand TikTok/Instagram/YouTube/X/LinkedIn posts with AI (media metadata, niche discovery, hook & viral analysis)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",