@orchyn/mcp 1.15.0 → 1.16.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,23 @@ 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.15.0";
14
+ export const MCP_SERVER_VERSION = "1.16.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
- /** Single UI resource URI shared by all tools */
19
+ /** Default UI resource URI (only used when a tool has no explicit view). */
20
20
  const UI_RESOURCE_URI = "ui://orchyn/view";
21
+ /**
22
+ * Distinct UI resource URI per tool/view. Claude/ChatGPT create one app
23
+ * instance per resourceUri and key app state by it (ext-apps#558), so giving
24
+ * each tool its own uri avoids a shared app instance / session colliding
25
+ * between different tools and lets each view complete its own handshake.
26
+ */
27
+ function uiResource(tool) {
28
+ const slug = tool.replace(/[^a-z0-9_]/gi, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
29
+ return `ui://orchyn/${slug || "view"}`;
30
+ }
21
31
  /**
22
32
  * claude.ai requires `ui.domain` on the resource == sha256("<MCP endpoint
23
33
  * URL>")[:32] + ".claudemcpcontent.com" — the iframe is only revealed on
@@ -234,49 +244,68 @@ export function createMcpServer(makeClient) {
234
244
  },
235
245
  },
236
246
  });
237
- // Register the single UI resource that all tools share.
238
- // The host reads this resource, renders it in a sandboxed iframe,
239
- // and pushes tool results via ui/notifications/tool-result.
240
- server.registerResource("Orchyn Interactive View", UI_RESOURCE_URI, { mimeType: RESOURCE_MIME_TYPE }, async () => {
241
- // Build CSP resourceDomains from env so proxied thumbnails work
242
- const domains = [
243
- "https://*.tiktokcdn.com",
244
- "https://*.cdninstagram.com",
245
- "https://*.ytimg.com",
246
- "https://*.googlevideo.com",
247
- "https://*.licdn.com",
248
- "https://*.linkedin.com",
249
- ];
250
- const apiUrl = process.env.ORCHYN_API_URL || process.env.ORCHYN_BASE_URL;
251
- if (apiUrl && apiUrl.trim()) {
252
- domains.push(apiUrl.trim().replace(/\/+$/, ""));
253
- }
254
- const domain = await computeAppDomain();
255
- return {
256
- contents: [
257
- {
258
- uri: UI_RESOURCE_URI,
259
- mimeType: RESOURCE_MIME_TYPE,
260
- text: ORCHYN_UI_TEMPLATE,
261
- _meta: {
262
- ui: {
263
- ...(domain ? { domain } : {}),
264
- csp: {
265
- resourceDomains: domains,
247
+ // Register one UI app resource per tool/view. Claude/ChatGPT render a
248
+ // separate sandboxed app per resourceUri and key app state by it, so a
249
+ // distinct URI per tool avoids a shared app instance/session colliding
250
+ // between different tools (ext-apps#558). Each URI serves the same generic
251
+ // template, which renders whichever structuredResult the tool delivers.
252
+ const TOOL_URIS = [
253
+ "analyze_post",
254
+ "get_social_media",
255
+ "discover_social_posts",
256
+ "get_user_posts",
257
+ "analyze_creator_profile",
258
+ "get_post_comments",
259
+ "search_creators",
260
+ "get_similar_creators",
261
+ "discover_sounds",
262
+ "check_orchyn_credits",
263
+ "buy_orchyn_credits",
264
+ "understand_social_post",
265
+ ].map(uiResource);
266
+ // Build CSP resourceDomains from env so proxied thumbnails work
267
+ const domains = [
268
+ "https://*.tiktokcdn.com",
269
+ "https://*.cdninstagram.com",
270
+ "https://*.ytimg.com",
271
+ "https://*.googlevideo.com",
272
+ "https://*.licdn.com",
273
+ "https://*.linkedin.com",
274
+ ];
275
+ const apiUrl = process.env.ORCHYN_API_URL || process.env.ORCHYN_BASE_URL;
276
+ if (apiUrl && apiUrl.trim()) {
277
+ domains.push(apiUrl.trim().replace(/\/+$/, ""));
278
+ }
279
+ for (const uri of TOOL_URIS) {
280
+ server.registerResource("Orchyn Interactive View", uri, { mimeType: RESOURCE_MIME_TYPE }, async () => {
281
+ // Computed per-request so the claude.ai sandbox origin is correct.
282
+ const domain = await computeAppDomain();
283
+ return {
284
+ contents: [
285
+ {
286
+ uri,
287
+ mimeType: RESOURCE_MIME_TYPE,
288
+ text: ORCHYN_UI_TEMPLATE,
289
+ _meta: {
290
+ ui: {
291
+ ...(domain ? { domain } : {}),
292
+ csp: {
293
+ resourceDomains: domains,
294
+ },
295
+ prefersBorder: false,
266
296
  },
267
- prefersBorder: false,
268
297
  },
269
298
  },
270
- },
271
- ],
272
- };
273
- });
299
+ ],
300
+ };
301
+ });
302
+ }
274
303
  server.registerTool("analyze_post", {
275
304
  title: "Analyze Post",
276
305
  description: "Analyze a social post (video, image, carousel/slideshow) from its link — " +
277
306
  "imports the media and runs AI analysis over the actual content (video frames, carousel images, caption). " +
278
307
  "Supports TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu and Bilibili. Returns the full analysis once finished. First use free per user.",
279
- _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
308
+ _meta: { ui: { resourceUri: uiResource("analyze_post") }, "ui/resourceUri": uiResource("analyze_post") },
280
309
  inputSchema: z
281
310
  .object({
282
311
  url: z.string().describe("Public post URL (TikTok/Instagram/YouTube/X, Douyin, Xiaohongshu or Bilibili)."),
@@ -332,7 +361,7 @@ export function createMcpServer(makeClient) {
332
361
  description: "Fetch a social post's media from a TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu or Bilibili URL: " +
333
362
  "contentType (video/image/carousel/slideshow), title, caption, author, stats and direct media URLs. " +
334
363
  "Returns an inline thumbnail image. Consumes 1 orchyn credit. First use free per user.",
335
- _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
364
+ _meta: { ui: { resourceUri: uiResource("get_social_media") }, "ui/resourceUri": uiResource("get_social_media") },
336
365
  inputSchema: z
337
366
  .object({
338
367
  url: z.string().describe("Full public post URL."),
@@ -352,7 +381,7 @@ export function createMcpServer(makeClient) {
352
381
  description: "Discover recent posts (video, image, carousel, slideshow) for a niche on YouTube, TikTok, Instagram, Douyin, Xiaohongshu, X/Twitter or Bilibili. " +
353
382
  "Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
354
383
  '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 }, "ui/resourceUri": UI_RESOURCE_URI },
384
+ _meta: { ui: { resourceUri: uiResource("discover_social_posts") }, "ui/resourceUri": uiResource("discover_social_posts") },
356
385
  inputSchema: z
357
386
  .object({
358
387
  niche: z.string().describe("Niche/topic, e.g. 'fitness'."),
@@ -379,7 +408,7 @@ export function createMcpServer(makeClient) {
379
408
  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
409
  "Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
381
410
  "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 }, "ui/resourceUri": UI_RESOURCE_URI },
411
+ _meta: { ui: { resourceUri: uiResource("get_user_posts") }, "ui/resourceUri": uiResource("get_user_posts") },
383
412
  inputSchema: z
384
413
  .object({
385
414
  username: z.string().describe("Creator handle, e.g. 'zoundsapp' or '@zoundsapp'."),
@@ -404,7 +433,7 @@ export function createMcpServer(makeClient) {
404
433
  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
434
  "then synthesize a profile report — creator summary, niche, content themes, hook styles, strengths/weaknesses, " +
406
435
  "engagement patterns, audience insights, variation ideas, collaboration fit. Consumes 15 orchyn credits. First use free per user.",
407
- _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
436
+ _meta: { ui: { resourceUri: uiResource("analyze_creator_profile") }, "ui/resourceUri": uiResource("analyze_creator_profile") },
408
437
  inputSchema: z
409
438
  .object({
410
439
  username: z.string().describe("Creator handle, e.g. 'zoundsapp'."),
@@ -429,7 +458,7 @@ export function createMcpServer(makeClient) {
429
458
  title: "Get Post Comments",
430
459
  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
460
  "when available — audience sentiment/audience-signal analysis. Consumes 2 orchyn credits. First use free per user.",
432
- _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
461
+ _meta: { ui: { resourceUri: uiResource("get_post_comments") }, "ui/resourceUri": uiResource("get_post_comments") },
433
462
  inputSchema: z
434
463
  .object({
435
464
  url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/Douyin/X/Bilibili/LinkedIn)."),
@@ -449,7 +478,7 @@ export function createMcpServer(makeClient) {
449
478
  title: "Search Creators",
450
479
  description: "Search creators by niche/keyword on TikTok, Instagram, Xiaohongshu, YouTube or Douyin — username, nickname, follower count, " +
451
480
  "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 }, "ui/resourceUri": UI_RESOURCE_URI },
481
+ _meta: { ui: { resourceUri: uiResource("search_creators") }, "ui/resourceUri": uiResource("search_creators") },
453
482
  inputSchema: z
454
483
  .object({
455
484
  keyword: z.string().describe("Niche/keyword, e.g. 'fitness' or a creator name."),
@@ -473,7 +502,7 @@ export function createMcpServer(makeClient) {
473
502
  title: "Get Similar Creators",
474
503
  description: "Find lookalike creators for a given handle — TikTok similar-user recommendations or Instagram " +
475
504
  "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 }, "ui/resourceUri": UI_RESOURCE_URI },
505
+ _meta: { ui: { resourceUri: uiResource("get_similar_creators") }, "ui/resourceUri": uiResource("get_similar_creators") },
477
506
  inputSchema: z
478
507
  .object({
479
508
  username: z.string().describe("Seed creator handle, e.g. 'zoundsapp'."),
@@ -493,7 +522,7 @@ export function createMcpServer(makeClient) {
493
522
  title: "Discover Sounds",
494
523
  description: "Discover trending sounds/music for a keyword on TikTok or Instagram — the sound is a huge ranking " +
495
524
  "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 }, "ui/resourceUri": UI_RESOURCE_URI },
525
+ _meta: { ui: { resourceUri: uiResource("discover_sounds") }, "ui/resourceUri": uiResource("discover_sounds") },
497
526
  inputSchema: z
498
527
  .object({
499
528
  keyword: z.string().describe("Niche/keyword, e.g. 'gym'."),
@@ -513,7 +542,7 @@ export function createMcpServer(makeClient) {
513
542
  server.registerTool("check_orchyn_credits", {
514
543
  title: "Check Orchyn Credits",
515
544
  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 }, "ui/resourceUri": UI_RESOURCE_URI },
545
+ _meta: { ui: { resourceUri: uiResource("check_orchyn_credits") }, "ui/resourceUri": uiResource("check_orchyn_credits") },
517
546
  inputSchema: z.object({}).strict(),
518
547
  }, async (_args, extra) => {
519
548
  const client = await makeClient(extra);
@@ -527,7 +556,7 @@ export function createMcpServer(makeClient) {
527
556
  server.registerTool("buy_orchyn_credits", {
528
557
  title: "Buy Orchyn Credits",
529
558
  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 }, "ui/resourceUri": UI_RESOURCE_URI },
559
+ _meta: { ui: { resourceUri: uiResource("buy_orchyn_credits") }, "ui/resourceUri": uiResource("buy_orchyn_credits") },
531
560
  inputSchema: z.object({}).strict(),
532
561
  }, async (_args, extra) => {
533
562
  const client = await makeClient(extra);
@@ -543,7 +572,7 @@ export function createMcpServer(makeClient) {
543
572
  description: "Import a social post URL AND understand it with multimodal AI over the actual video/images: " +
544
573
  "summary, hook strength, viral triggers, format breakdown and variation ideas. Includes the thumbnail. " +
545
574
  "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 }, "ui/resourceUri": UI_RESOURCE_URI },
575
+ _meta: { ui: { resourceUri: uiResource("understand_social_post") }, "ui/resourceUri": uiResource("understand_social_post") },
547
576
  inputSchema: z
548
577
  .object({
549
578
  url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/X/Douyin/Xiaohongshu/Bilibili)."),
@@ -526,10 +526,41 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
526
526
  }
527
527
 
528
528
  // ─── Render ───
529
+ // Hosts deliver the tool result in two shapes:
530
+ // - { structuredContent: {...} } (spec-optional, used when present)
531
+ // - { content: [ {type,text,...} ] } (standard MCP content blocks)
532
+ // Our results put the HTML card prefix + structured JSON inside the first
533
+ // text content block, so decode BOTH. If the result is a primitive/string,
534
+ // surface it as-is.
535
+ function extractResult(result){
536
+ if(!result||typeof result!=="object")return result;
537
+ var sc=result.structuredContent;
538
+ if(sc&&typeof sc==="object"&&!Array.isArray(sc))return sc;
539
+ if(Array.isArray(result.content)){
540
+ var textBlock=null;
541
+ for(var i=0;i<result.content.length;i++){
542
+ var c=result.content[i];
543
+ if(c&&c.type==="text"&&typeof c.text==="string"){textBlock=c.text;break;}
544
+ }
545
+ if(typeof textBlock==="string"&&textBlock!==""){
546
+ // Split "<html>\n\n{json}" — if there's a JSON object after the HTML,
547
+ // parse it; otherwise show the text itself.
548
+ var m=/^[\s]*<[^>]+>[\s\S]*?\n\n(\{.*\})[\s]*$/m.exec(textBlock);
549
+ if(m){try{var parsed=JSON.parse(m[1]);if(parsed&&typeof parsed==="object")return parsed;}catch(e){}}
550
+ // Single-line HTML (common): keep the whole block as text fallback.
551
+ if(textBlock.indexOf("<")===0){return { _html: textBlock };}
552
+ try{var j=JSON.parse(textBlock);if(j&&typeof j==="object")return j;}catch(e2){}
553
+ }
554
+ }
555
+ return result;
556
+ }
557
+
529
558
  function render(result){
530
559
  var app=document.getElementById("app");if(!app)return;
531
- var d=result&&result.structuredContent?result.structuredContent:result;
560
+ var d=extractResult(result);
532
561
  if(!d||typeof d!=="object"){app.innerHTML='<div class="json-block fade-in">'+esc(JSON.stringify(d,null,2))+"</div>";return;}
562
+ // Raw HTML-only result — inject it directly.
563
+ if(d._html){app.innerHTML=d._html;setTimeout(reportSize,50);return;}
533
564
 
534
565
  // Analysis
535
566
  if(d.analysis&&(d.post||d.url)){app.innerHTML=analysisCard(d);return;}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchyn/mcp",
3
- "version": "1.15.0",
3
+ "version": "1.16.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",