@orchyn/mcp 1.13.0 → 1.15.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.
- package/dist/shared/tools.js +47 -47
- package/dist/shared/ui-template.js +32 -4
- package/package.json +1 -1
package/dist/shared/tools.js
CHANGED
|
@@ -11,29 +11,34 @@ 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.
|
|
14
|
+
export const MCP_SERVER_VERSION = "1.15.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
|
-
|
|
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;
|
|
23
32
|
try {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
31
|
-
bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
32
|
-
}
|
|
33
|
-
return { type: "image", data: btoa(bin), mimeType };
|
|
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`;
|
|
34
39
|
}
|
|
35
40
|
catch {
|
|
36
|
-
return
|
|
41
|
+
return undefined;
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
@@ -82,17 +87,12 @@ function proxyUrls(obj) {
|
|
|
82
87
|
return obj;
|
|
83
88
|
}
|
|
84
89
|
async function toToolResult(proxy) {
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
? fetchAsBase64Image(String(c.url), String(c.mimeType ?? "image/jpeg"))
|
|
92
|
-
: Promise.resolve({ type: "image", data: String(c.data ?? ""), mimeType: String(c.mimeType ?? "image/jpeg") })));
|
|
93
|
-
// The Rust backend embeds HTML cards directly in the text block
|
|
94
|
-
// (type:"text", text:"<div>...</div>\n\n{json}"). We extract the HTML
|
|
95
|
-
// prefix from the first text contentBlock and prepend it.
|
|
90
|
+
// MCP Apps views (ChatGPT & Claude) render the interactive HTML card, which
|
|
91
|
+
// already embeds all thumbnails/videos. Claude's Apps bridge rejects a tool
|
|
92
|
+
// result that mixes raw `image` blocks with an app view ("could not be
|
|
93
|
+
// processed: Error processing image" + blank iframe), so we intentionally
|
|
94
|
+
// drop the standalone base64 image blocks here and keep only the text block
|
|
95
|
+
// carrying the HTML card + structured JSON.
|
|
96
96
|
const textBlock = proxy.contentBlocks.find((c) => c.type === "text");
|
|
97
97
|
const rawText = textBlock ? String(textBlock.text ?? "") : "";
|
|
98
98
|
const htmlPrefix = rawText.startsWith("<")
|
|
@@ -106,7 +106,7 @@ async function toToolResult(proxy) {
|
|
|
106
106
|
const proxiedHtml = htmlPrefix ? proxyImageUrlsInHtml(htmlPrefix) : "";
|
|
107
107
|
const text = proxiedHtml ? `${proxiedHtml}\n\n${textJson}` : textJson;
|
|
108
108
|
return {
|
|
109
|
-
content: [
|
|
109
|
+
content: [{ type: "text", text }],
|
|
110
110
|
structuredContent: proxied ?? {},
|
|
111
111
|
};
|
|
112
112
|
}
|
|
@@ -251,6 +251,7 @@ export function createMcpServer(makeClient) {
|
|
|
251
251
|
if (apiUrl && apiUrl.trim()) {
|
|
252
252
|
domains.push(apiUrl.trim().replace(/\/+$/, ""));
|
|
253
253
|
}
|
|
254
|
+
const domain = await computeAppDomain();
|
|
254
255
|
return {
|
|
255
256
|
contents: [
|
|
256
257
|
{
|
|
@@ -259,9 +260,11 @@ export function createMcpServer(makeClient) {
|
|
|
259
260
|
text: ORCHYN_UI_TEMPLATE,
|
|
260
261
|
_meta: {
|
|
261
262
|
ui: {
|
|
263
|
+
...(domain ? { domain } : {}),
|
|
262
264
|
csp: {
|
|
263
265
|
resourceDomains: domains,
|
|
264
266
|
},
|
|
267
|
+
prefersBorder: false,
|
|
265
268
|
},
|
|
266
269
|
},
|
|
267
270
|
},
|
|
@@ -273,7 +276,7 @@ export function createMcpServer(makeClient) {
|
|
|
273
276
|
description: "Analyze a social post (video, image, carousel/slideshow) from its link — " +
|
|
274
277
|
"imports the media and runs AI analysis over the actual content (video frames, carousel images, caption). " +
|
|
275
278
|
"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 } },
|
|
279
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
277
280
|
inputSchema: z
|
|
278
281
|
.object({
|
|
279
282
|
url: z.string().describe("Public post URL (TikTok/Instagram/YouTube/X, Douyin, Xiaohongshu or Bilibili)."),
|
|
@@ -302,17 +305,14 @@ export function createMcpServer(makeClient) {
|
|
|
302
305
|
const creatorHandle = String(post.creatorHandle ?? "");
|
|
303
306
|
const title = String(post.title ?? post.caption ?? "");
|
|
304
307
|
const htmlCard = buildAnalysisHtmlCard(proxied, thumbnailUrl, platform, creatorHandle, title);
|
|
305
|
-
// The
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
.map((img) => "url" in img && img.url
|
|
311
|
-
? fetchAsBase64Image(String(img.url), String(img.mimeType ?? "image/jpeg"))
|
|
312
|
-
: Promise.resolve({ type: "image", data: String(img.data ?? ""), mimeType: String(img.mimeType ?? "image/jpeg") })));
|
|
308
|
+
// The interactive HTML card above already embeds the thumbnail + full
|
|
309
|
+
// video, so no standalone base64 `image` blocks are emitted. Claude's Apps
|
|
310
|
+
// bridge rejects raw image blocks mixed with an app view ("could not be
|
|
311
|
+
// processed: Error processing image" + blank iframe), so we keep only the
|
|
312
|
+
// text block carrying the card + structured JSON.
|
|
313
313
|
const textJson = JSON.stringify(proxied, null, 2);
|
|
314
314
|
return {
|
|
315
|
-
content: [
|
|
315
|
+
content: [{ type: "text", text: `${htmlCard}\n\n${textJson}` }],
|
|
316
316
|
structuredContent: proxied,
|
|
317
317
|
};
|
|
318
318
|
}
|
|
@@ -332,7 +332,7 @@ export function createMcpServer(makeClient) {
|
|
|
332
332
|
description: "Fetch a social post's media from a TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu or Bilibili URL: " +
|
|
333
333
|
"contentType (video/image/carousel/slideshow), title, caption, author, stats and direct media URLs. " +
|
|
334
334
|
"Returns an inline thumbnail image. Consumes 1 orchyn credit. First use free per user.",
|
|
335
|
-
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
335
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
336
336
|
inputSchema: z
|
|
337
337
|
.object({
|
|
338
338
|
url: z.string().describe("Full public post URL."),
|
|
@@ -352,7 +352,7 @@ export function createMcpServer(makeClient) {
|
|
|
352
352
|
description: "Discover recent posts (video, image, carousel, slideshow) for a niche on YouTube, TikTok, Instagram, Douyin, Xiaohongshu, X/Twitter or Bilibili. " +
|
|
353
353
|
"Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
|
|
354
354
|
'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 } },
|
|
355
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
356
356
|
inputSchema: z
|
|
357
357
|
.object({
|
|
358
358
|
niche: z.string().describe("Niche/topic, e.g. 'fitness'."),
|
|
@@ -379,7 +379,7 @@ export function createMcpServer(makeClient) {
|
|
|
379
379
|
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
380
|
"Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
|
|
381
381
|
"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 } },
|
|
382
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
383
383
|
inputSchema: z
|
|
384
384
|
.object({
|
|
385
385
|
username: z.string().describe("Creator handle, e.g. 'zoundsapp' or '@zoundsapp'."),
|
|
@@ -404,7 +404,7 @@ export function createMcpServer(makeClient) {
|
|
|
404
404
|
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
405
|
"then synthesize a profile report — creator summary, niche, content themes, hook styles, strengths/weaknesses, " +
|
|
406
406
|
"engagement patterns, audience insights, variation ideas, collaboration fit. Consumes 15 orchyn credits. First use free per user.",
|
|
407
|
-
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
407
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
408
408
|
inputSchema: z
|
|
409
409
|
.object({
|
|
410
410
|
username: z.string().describe("Creator handle, e.g. 'zoundsapp'."),
|
|
@@ -429,7 +429,7 @@ export function createMcpServer(makeClient) {
|
|
|
429
429
|
title: "Get Post Comments",
|
|
430
430
|
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
431
|
"when available — audience sentiment/audience-signal analysis. Consumes 2 orchyn credits. First use free per user.",
|
|
432
|
-
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
432
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
433
433
|
inputSchema: z
|
|
434
434
|
.object({
|
|
435
435
|
url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/Douyin/X/Bilibili/LinkedIn)."),
|
|
@@ -449,7 +449,7 @@ export function createMcpServer(makeClient) {
|
|
|
449
449
|
title: "Search Creators",
|
|
450
450
|
description: "Search creators by niche/keyword on TikTok, Instagram, Xiaohongshu, YouTube or Douyin — username, nickname, follower count, " +
|
|
451
451
|
"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 } },
|
|
452
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
453
453
|
inputSchema: z
|
|
454
454
|
.object({
|
|
455
455
|
keyword: z.string().describe("Niche/keyword, e.g. 'fitness' or a creator name."),
|
|
@@ -473,7 +473,7 @@ export function createMcpServer(makeClient) {
|
|
|
473
473
|
title: "Get Similar Creators",
|
|
474
474
|
description: "Find lookalike creators for a given handle — TikTok similar-user recommendations or Instagram " +
|
|
475
475
|
"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 } },
|
|
476
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
477
477
|
inputSchema: z
|
|
478
478
|
.object({
|
|
479
479
|
username: z.string().describe("Seed creator handle, e.g. 'zoundsapp'."),
|
|
@@ -493,7 +493,7 @@ export function createMcpServer(makeClient) {
|
|
|
493
493
|
title: "Discover Sounds",
|
|
494
494
|
description: "Discover trending sounds/music for a keyword on TikTok or Instagram — the sound is a huge ranking " +
|
|
495
495
|
"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 } },
|
|
496
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
497
497
|
inputSchema: z
|
|
498
498
|
.object({
|
|
499
499
|
keyword: z.string().describe("Niche/keyword, e.g. 'gym'."),
|
|
@@ -513,7 +513,7 @@ export function createMcpServer(makeClient) {
|
|
|
513
513
|
server.registerTool("check_orchyn_credits", {
|
|
514
514
|
title: "Check Orchyn Credits",
|
|
515
515
|
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 } },
|
|
516
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
517
517
|
inputSchema: z.object({}).strict(),
|
|
518
518
|
}, async (_args, extra) => {
|
|
519
519
|
const client = await makeClient(extra);
|
|
@@ -527,7 +527,7 @@ export function createMcpServer(makeClient) {
|
|
|
527
527
|
server.registerTool("buy_orchyn_credits", {
|
|
528
528
|
title: "Buy Orchyn Credits",
|
|
529
529
|
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 } },
|
|
530
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
531
531
|
inputSchema: z.object({}).strict(),
|
|
532
532
|
}, async (_args, extra) => {
|
|
533
533
|
const client = await makeClient(extra);
|
|
@@ -543,7 +543,7 @@ export function createMcpServer(makeClient) {
|
|
|
543
543
|
description: "Import a social post URL AND understand it with multimodal AI over the actual video/images: " +
|
|
544
544
|
"summary, hook strength, viral triggers, format breakdown and variation ideas. Includes the thumbnail. " +
|
|
545
545
|
"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 } },
|
|
546
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
|
|
547
547
|
inputSchema: z
|
|
548
548
|
.object({
|
|
549
549
|
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(
|
|
278
|
-
|
|
279
|
-
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");}
|
|
@@ -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.
|
|
3
|
+
"version": "1.15.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",
|