@orchyn/mcp 1.12.3 → 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.
package/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  MCP (Model Context Protocol) server for [orchyn](https://orchyn.com) — fetch,
4
4
  discover and understand social posts (TikTok, Instagram, YouTube, X/Twitter,
5
- Douyin, Xiaohongshu, Bilibili) with your orchyn account and orchyn credits.
5
+ Douyin, Xiaohongshu, Bilibili, LinkedIn) with your orchyn account and orchyn
6
+ credits.
6
7
 
7
8
  ## Install (one link)
8
9
 
@@ -38,12 +39,12 @@ npx @orchyn/mcp login # one-time sign-in (Google)
38
39
 
39
40
  | Tool | Credits | Description |
40
41
  |------|---------|-------------|
41
- | `analyze_post` | first free* | **Preferred.** Analyze any post (video, image, carousel/slideshow) from a TikTok/Instagram/YouTube/X-Twitter/Douyin/Xiaohongshu/Bilibili URL — imports the media and runs AI analysis over the actual content (video frames, carousel images, caption). Returns a `jobId` to poll via `GET /ai/analyze-post`. *First analysis free per workspace via the dashboard free grant.* |
42
+ | `analyze_post` | first free* | **Preferred.** Analyze any post (video, image, carousel/slideshow, text) from a TikTok/Instagram/YouTube/X-Twitter/Douyin/Xiaohongshu/Bilibili/LinkedIn URL — imports the media and runs AI analysis over the actual content (video frames, carousel images, caption). Returns a `jobId` to poll via `GET /ai/analyze-post`. *First analysis free per workspace via the dashboard free grant.* |
42
43
  | `get_social_media` | 1 | Fetch a post's media from a URL: `contentType` (video/image/carousel/slideshow), title, caption, author, stats, direct media URLs **+ inline thumbnail image in chat**. |
43
44
  | `discover_social_posts` | 2 | **Preferred.** Find recent posts (video/image/carousel/slideshow) for a niche on YouTube, TikTok, Instagram, Douyin, Xiaohongshu, X/Twitter or Bilibili. Each post includes title/caption, views/likes/comments, author, `externalUrl` + **inline thumbnails (4 at a time)** — see *Images in chat* below. Supports `limit`/`offset` pagination (“next”). |
44
- | `get_user_posts` | 2 | List recent posts by a creator handle (e.g. `@zoundsapp`) on TikTok, Instagram, YouTube, Douyin, Xiaohongshu, X/Twitter or Bilibili — title/caption, `thumbnailUrl`, `externalUrl`, views/likes/comments, **inline thumbnails (up to 4)**. Use to scan a whole profile or spot patterns across an account. |
45
- | `analyze_creator_profile` | 15 | Deep-dive a whole creator profile on TikTok, Instagram, YouTube, Douyin, Xiaohongshu, X/Twitter or Bilibili: fetches recent posts, runs multimodal AI on up to 3, then synthesizes a profile report — creator summary, niche, content themes, hook styles, strengths/weaknesses, engagement patterns, audience insights, variation ideas, collaboration fit. |
46
- | `get_post_comments` | 2 | Fetch top comments for a post URL on TikTok, Instagram, YouTube, Douyin, X/Twitter or Bilibili, plus keyword clusters from TikTok Analytics when available — audience sentiment/audience-signal analysis. |
45
+ | `get_user_posts` | 2 | 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`) — title/caption, `thumbnailUrl`, `externalUrl`, views/likes/comments, **inline thumbnails (up to 4)**. Use to scan a whole profile or spot patterns across an account. |
46
+ | `analyze_creator_profile` | 15 | Deep-dive a whole creator profile on TikTok, Instagram, YouTube, Douyin, Xiaohongshu, X/Twitter, Bilibili or LinkedIn: fetches recent posts, runs multimodal AI on up to 3, then synthesizes a profile report — creator summary, niche, content themes, hook styles, strengths/weaknesses, engagement patterns, audience insights, variation ideas, collaboration fit. |
47
+ | `get_post_comments` | 2 | Fetch top comments for a post URL on TikTok, Instagram, YouTube, Douyin, X/Twitter, Bilibili or LinkedIn, plus keyword clusters from TikTok Analytics when available — audience sentiment/audience-signal analysis. |
47
48
  | `search_creators` | 2 | Search creators by niche/keyword on TikTok, Instagram, Xiaohongshu, YouTube or Douyin — username, nickname, follower count, signature, verified status. Use to find influencers to vet or analyze. |
48
49
  | `get_similar_creators` | 2 | Find lookalike creators for a given handle (TikTok similar-user recommendations or Instagram similar users) — “if this creator works, here are more like them”. |
49
50
  | `discover_sounds` | 2 | Discover trending sounds/music for a keyword on TikTok or Instagram — sound choice is a huge ranking signal for TikTok virality. Returns title, artist, duration, play/cover URLs. |
@@ -57,9 +58,23 @@ All tools require a connected orchyn account and are billed against your orchyn
57
58
 
58
59
  `*` `analyze_post` also bills against **workspace** credits (first free grant); the other tools bill against **per-user MCP** credits (never tied to an app/workspace).
59
60
 
60
- ## Images in Claude / ChatGPT chat
61
+ ## Interactive cards in Claude / ChatGPT chat
61
62
 
62
- Every tool that returns posts also renders **inline thumbnails** directly in the chat:
63
+ Every tool that returns posts also renders **inline interactive cards** directly
64
+ in the chat (MCP Apps `ui://orchyn/view` resource rendered in a sandboxed
65
+ iframe):
66
+
67
+ - **Video posts** (TikTok/IG/YouTube/Douyin/LinkedIn) — an inline `<video>`
68
+ player with the thumbnail as poster, playing the re-hosted permanent MP4
69
+ (no expiring CDN tokens).
70
+ - **Carousels / slideshows** — a horizontally scrollable strip of every slide
71
+ with an image count chip.
72
+ - **Single images** — inline thumbnail.
73
+ - **Text-only posts** (LinkedIn / X) — a styled quote block of the post text.
74
+ - **Official brand marks** — each card shows the platform's real logo
75
+ (simple-icons) in its brand color instead of an emoji.
76
+
77
+ Also **inline thumbnails** render for the model / plain-text clients:
63
78
 
64
79
  - `get_social_media` / `understand_social_post` / `analyze_post` — up to 4 frames inline (poster + carousel slides). The full `mediaItems[].preview_url` + `thumbnailUrl` stay in `structuredContent` for the model to reason over.
65
80
  - `discover_social_posts` / `get_user_posts` / `analyze_creator_profile` — each returned post shows its thumbnail inline (up to 4 at once) together with its title/caption + views/likes/comments. Say **"next"** or **"show more"** — Claude will re-call with `offset`/`limit` pagination. Say **"analyze the 2nd one"** — Claude calls `analyze_post` or `understand_social_post` on that URL.
@@ -256,8 +271,10 @@ per the MCP 2025-03-26 spec):
256
271
  - Douyin: `douyin.com/*`
257
272
  - Xiaohongshu: `xiaohongshu.com/*`, `xhslink.com/*`
258
273
  - Bilibili: `bilibili.com/*`, `b23.tv/*`
274
+ - LinkedIn: `linkedin.com/*` (posts, profile URLs)
259
275
 
260
- All tools accept these hosts and handle **video, image, carousel and slideshow** posts.
276
+ All tools accept these hosts and handle **video, image, carousel, slideshow
277
+ and text** posts.
261
278
 
262
279
  ## Troubleshooting
263
280
 
@@ -302,7 +319,7 @@ package for **Claude Desktop**, **Cursor**, and **OpenAI Agents SDK**.
302
319
  ```bash
303
320
  npm install
304
321
  npm run build # tsc
305
- npm test # vitest (54 unit tests, mocked fetch — no network)
322
+ npm test # vitest (66 unit tests, mocked fetch — no network)
306
323
  ```
307
324
 
308
325
  ## License
@@ -11,13 +11,54 @@ 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.12.2";
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
+ }
44
+ /** Convert an arbitrary http(s) URL to a base64 MCP `image` data block. */
45
+ async function fetchAsBase64Image(url, mimeType) {
46
+ try {
47
+ const res = await fetch(url, { signal: AbortSignal.timeout(12_000) });
48
+ if (!res.ok)
49
+ return { type: "image", data: "", mimeType };
50
+ const buf = await res.arrayBuffer();
51
+ const bytes = new Uint8Array(buf);
52
+ let bin = "";
53
+ for (let i = 0; i < bytes.length; i += 0x8000) {
54
+ bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
55
+ }
56
+ return { type: "image", data: btoa(bin), mimeType };
57
+ }
58
+ catch {
59
+ return { type: "image", data: "", mimeType };
60
+ }
61
+ }
21
62
  /**
22
63
  * Rewrite external image URLs to go through the orchyn proxy so they
23
64
  * work inside ChatGPT's sandboxed iframe (CORS + CSP restrictions).
@@ -52,7 +93,7 @@ function proxyUrls(obj) {
52
93
  if (obj && typeof obj === "object") {
53
94
  const out = {};
54
95
  for (const [k, v] of Object.entries(obj)) {
55
- if (["thumbnailUrl", "preview_url", "coverUrl", "avatarUrl", "avatar_thumb", "image_url", "url"].includes(k) && typeof v === "string") {
96
+ if (["thumbnailUrl", "preview_url", "coverUrl", "avatarUrl", "avatar_thumb", "image_url", "videoUrl", "video_url", "url"].includes(k) && typeof v === "string") {
56
97
  out[k] = proxyImageUrl(v);
57
98
  }
58
99
  else {
@@ -63,14 +104,17 @@ function proxyUrls(obj) {
63
104
  }
64
105
  return obj;
65
106
  }
66
- function toToolResult(proxy) {
67
- const images = proxy.contentBlocks
68
- .filter((c) => c.type === "image")
69
- .map((c) => ({
70
- type: "image",
71
- data: String(c.data ?? ""),
72
- mimeType: String(c.mimeType ?? "image/jpeg"),
73
- }));
107
+ async function toToolResult(proxy) {
108
+ // The MCP SDK's ImageContent only accepts `data` (base64). The orchyn
109
+ // backend now emits `url` image blocks pointing at permanent, re-hosted
110
+ // JPEG thumbnails (HEIC already transcoded), so convert those URLs to
111
+ // base64 data here — guaranteeing clients render a valid image.
112
+ const imgBlocks = proxy.contentBlocks.filter((c) => c.type === "image");
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
116
+ ? fetchAsBase64Image(String(c.url), 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);
74
118
  // The Rust backend embeds HTML cards directly in the text block
75
119
  // (type:"text", text:"<div>...</div>\n\n{json}"). We extract the HTML
76
120
  // prefix from the first text contentBlock and prepend it.
@@ -91,9 +135,9 @@ function toToolResult(proxy) {
91
135
  structuredContent: proxied ?? {},
92
136
  };
93
137
  }
94
- /** Replace external image src/href URLs in HTML with proxied versions. */
138
+ /** Replace external image/video src, poster and href URLs in HTML with proxied versions. */
95
139
  function proxyImageUrlsInHtml(html) {
96
- return html.replace(/(src|href)="(https?:\/\/[^"']+?)"/g, (_match, attr, url) => {
140
+ return html.replace(/(src|href|poster)="(https?:\/\/[^"']+?)"/g, (_match, attr, url) => {
97
141
  return `${attr}="${proxyImageUrl(url)}"`;
98
142
  });
99
143
  }
@@ -101,19 +145,36 @@ function toolError(prefix, err) {
101
145
  const msg = err instanceof Error ? err.message : String(err);
102
146
  return { content: [{ type: "text", text: `${prefix}: ${msg}` }], isError: true };
103
147
  }
104
- /** Build an HTML card for analyze_post results. */
105
- function buildAnalysisHtmlCard(result, thumbnailUrl, platform, creatorHandle, title) {
106
- const platformEmoji = {
107
- tiktok: "🎵", instagram: "📸", youtube: "▶️", twitter: "🐦",
108
- x: "🐦", douyin: "🎵", xiaohongshu: "📕", bilibili: "📺",
148
+ /** Official brand color per platform (matches ui-template.ts pColor). */
149
+ function platformBrandColor(p) {
150
+ const map = {
151
+ tiktok: "#000000", douyin: "#000000", instagram: "#E4405F",
152
+ youtube: "#FF0000", xiaohongshu: "#FF2442", x: "#000000",
153
+ twitter: "#1DA1F2", bilibili: "#00A1D6", linkedin: "#0A66C2",
109
154
  };
110
- const platformColor = {
111
- tiktok: "#000000", instagram: "#E4405F", youtube: "#FF0000",
112
- twitter: "#1DA1F2", x: "#000000", douyin: "#000000",
113
- xiaohongshu: "#FF2442", bilibili: "#00A1D6",
155
+ return map[p] || "#6b7280";
156
+ }
157
+ /** Official brand mark (simple-icons path) as an inline SVG inheriting currentColor. */
158
+ function platformSvgMark(p, size = 14) {
159
+ const paths = {
160
+ tiktok: "M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z",
161
+ douyin: "M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z",
162
+ instagram: "M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077",
163
+ youtube: "M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z",
164
+ x: "M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z",
165
+ twitter: "M21.543 7.104c.015.211.015.423.015.636 0 6.507-4.954 14.01-14.01 14.01v-.003A13.94 13.94 0 0 1 0 19.539a9.88 9.88 0 0 0 7.287-2.041 4.93 4.93 0 0 1-4.6-3.42 4.916 4.916 0 0 0 2.223-.084A4.926 4.926 0 0 1 .96 9.167v-.062a4.887 4.887 0 0 0 2.235.616A4.928 4.928 0 0 1 1.67 3.148 13.98 13.98 0 0 0 11.82 8.292a4.929 4.929 0 0 1 8.39-4.49 9.868 9.868 0 0 0 3.128-1.196 4.941 4.941 0 0 1-2.165 2.724A9.828 9.828 0 0 0 24 4.555a10.019 10.019 0 0 1-2.457 2.549z",
166
+ xiaohongshu: "M22.405 9.879c.002.016.01.02.07.019h.725a.797.797 0 0 0 .78-.972.794.794 0 0 0-.884-.618.795.795 0 0 0-.692.794c0 .101-.002.666.001.777zm-11.509 4.808c-.203.001-1.353.004-1.685.003a2.528 2.528 0 0 1-.766-.126.025.025 0 0 0-.03.014L7.7 16.127a.025.025 0 0 0 .01.032c.111.06.336.124.495.124.66.01 1.32.002 1.981 0 .01 0 .02-.006.023-.015l.712-1.545a.025.025 0 0 0-.024-.036zM.477 9.91c-.071 0-.076.002-.076.01a.834.834 0 0 0-.01.08c-.027.397-.038.495-.234 3.06-.012.24-.034.389-.135.607-.026.057-.033.042.003.112.046.092.681 1.523.787 1.74.008.015.011.02.017.02.008 0 .033-.026.047-.044.147-.187.268-.391.371-.606.306-.635.44-1.325.486-1.706.014-.11.021-.22.03-.33l.204-2.616.022-.293c.003-.029 0-.033-.03-.034zm7.203 3.757a1.427 1.427 0 0 1-.135-.607c-.004-.084-.031-.39-.235-3.06a.443.443 0 0 0-.01-.082c-.004-.011-.052-.008-.076-.008h-1.48c-.03.001-.034.005-.03.034l.021.293c.076.982.153 1.964.233 2.946.05.4.186 1.085.487 1.706.103.215.223.419.37.606.015.018.037.051.048.049.02-.003.742-1.642.804-1.765.036-.07.03-.055.003-.112zm3.861-.913h-.872a.126.126 0 0 1-.116-.178l1.178-2.625a.025.025 0 0 0-.023-.035l-1.318-.003a.148.148 0 0 1-.135-.21l.876-1.954a.025.025 0 0 0-.023-.035h-1.56c-.01 0-.02.006-.024.015l-.926 2.068c-.085.169-.314.634-.399.938a.534.534 0 0 0-.02.191.46.46 0 0 0 .23.378.981.981 0 0 0 .46.119h.59c.041 0-.688 1.482-.834 1.972a.53.53 0 0 0-.023.172.465.465 0 0 0 .23.398c.15.092.342.12.475.12l1.66-.001c.01 0 .02-.006.023-.015l.575-1.28a.025.025 0 0 0-.024-.035zm-6.93-4.937H3.1a.032.032 0 0 0-.034.033c0 1.048-.01 2.795-.01 6.829 0 .288-.269.262-.28.262h-.74c-.04.001-.044.004-.04.047.001.037.465 1.064.555 1.263.01.02.03.033.051.033.157.003.767.009.938-.014.153-.02.3-.06.438-.132.3-.156.49-.419.595-.765.052-.172.075-.353.075-.533.002-2.33 0-4.66-.007-6.991a.032.032 0 0 0-.032-.032zm11.784 6.896c0-.014-.01-.021-.024-.022h-1.465c-.048-.001-.049-.002-.05-.049v-4.66c0-.072-.005-.07.07-.07h.863c.08 0 .075.004.075-.074V8.393c0-.082.006-.076-.08-.076h-3.5c-.064 0-.075-.006-.075.073v1.445c0 .083-.006.077.08.077h.854c.075 0 .07-.004.07.07v4.624c0 .095.008.084-.085.084-.37 0-1.11-.002-1.304 0-.048.001-.06.03-.06.03l-.697 1.519s-.014.025-.008.036c.006.01.013.008.058.008 1.748.003 3.495.002 5.243.002.03-.001.034-.006.035-.033v-1.539zm4.177-3.43c0 .013-.007.023-.02.024-.346.006-.692.004-1.037.004-.014-.002-.022-.01-.022-.024-.005-.434-.007-.869-.01-1.303 0-.072-.006-.071.07-.07l.733-.003c.041 0 .081.002.12.015.093.025.16.107.165.204.006.431.002 1.153.001 1.153zm2.67.244a1.953 1.953 0 0 0-.883-.222h-.18c-.04-.001-.04-.003-.042-.04V10.21c0-.132-.007-.263-.025-.394a1.823 1.823 0 0 0-.153-.53 1.533 1.533 0 0 0-.677-.71 2.167 2.167 0 0 0-1-.258c-.153-.003-.567 0-.72 0-.07 0-.068.004-.068-.065V7.76c0-.031-.01-.041-.046-.039H17.93s-.016 0-.023.007c-.006.006-.008.012-.008.023v.546c-.008.036-.057.015-.082.022h-.95c-.022.002-.028.008-.03.032v1.481c0 .09-.004.082.082.082h.913c.082 0 .072.128.072.128V11.19s.003.117-.06.117h-1.482c-.068 0-.06.082-.06.082v1.445s-.01.068.064.068h1.457c.082 0 .076-.006.076.079v3.225c0 .088-.007.081.082.081h1.43c.09 0 .082.007.082-.08v-3.27c0-.029.006-.035.033-.035l2.323-.003c.098 0 .191.02.28.061a.46.46 0 0 1 .274.407c.008.395.003.79.003 1.185 0 .259-.107.367-.33.367h-1.218c-.023.002-.029.008-.028.033.184.437.374.871.57 1.303a.045.045 0 0 0 .04.026c.17.005.34.002.51.003.15-.002.517.004.666-.01a2.03 2.03 0 0 0 .408-.075c.59-.18.975-.698.976-1.313v-1.981c0-.128-.01-.254-.034-.38 0 .078-.029-.641-.724-.998z",
167
+ bilibili: "M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z",
168
+ linkedin: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z",
169
+ unknown: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71",
114
170
  };
115
- const emoji = platformEmoji[platform] || "📱";
116
- const color = platformColor[platform] || "#6b7280";
171
+ const d = paths[p] || paths.unknown;
172
+ return `<svg viewBox="0 0 24 24" width="${size}" height="${size}" fill="currentColor" style="vertical-align:-2px;flex-shrink:0;display:inline-block" aria-hidden="true"><path d="${d}"/></svg>`;
173
+ }
174
+ /** Build an HTML card for analyze_post results. */
175
+ function buildAnalysisHtmlCard(result, thumbnailUrl, platform, creatorHandle, title) {
176
+ const brandSvg = platformSvgMark(platform, 14);
177
+ const color = platformBrandColor(platform);
117
178
  const post = (result.post ?? {});
118
179
  const analysis = (result.analysis ?? {});
119
180
  const views = typeof post.views === "number" ? post.views.toLocaleString() : "—";
@@ -126,12 +187,19 @@ function buildAnalysisHtmlCard(result, thumbnailUrl, platform, creatorHandle, ti
126
187
  const viralTriggers = Array.isArray(analysis.viralTriggers) ? analysis.viralTriggers : [];
127
188
  const variationIdeas = Array.isArray(analysis.variationIdeas) ? analysis.variationIdeas.slice(0, 3) : [];
128
189
  const suggestedHook = typeof analysis.suggestedHook === "string" ? analysis.suggestedHook : "";
190
+ const videoUrl = typeof post.videoUrl === "string"
191
+ ? post.videoUrl
192
+ : Array.isArray(post.mediaItems)
193
+ ? post.mediaItems.find((m) => m.kind === "video" && typeof m.preview_url === "string")?.preview_url
194
+ : undefined;
129
195
  let html = `<div style="font-family:system-ui,-apple-system,sans-serif;max-width:600px;background:#0d1117;color:#e6edf3;border-radius:12px;overflow:hidden;margin:0 auto">`;
130
- // Thumbnail
131
- if (thumbnailUrl) {
132
- html += `<div style="position:relative"><img src="${thumbnailUrl}" style="width:100%;max-height:400px;object-fit:cover;display:block" onerror="this.style.display='none'" /><div style="position:absolute;bottom:12px;left:12px;display:flex;gap:8px;flex-wrap:wrap">`;
133
- html += `<span style="background:${color};color:#fff;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:600">${emoji} ${platform}</span>`;
134
- html += `</div></div>`;
196
+ // Thumbnail / video
197
+ const badgeHtml = `<span style="background:${color};color:#fff;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:600;display:inline-flex;align-items:center;gap:4px">${brandSvg} ${platform}</span>`;
198
+ if (videoUrl) {
199
+ html += `<div style="position:relative"><video src="${videoUrl}" controls preload="metadata" playsinline poster="${thumbnailUrl}" style="width:100%;max-height:400px;object-fit:contain;display:block;background:#000" onerror="this.outerHTML='<img src=&quot;${thumbnailUrl}&quot; style=&quot;width:100%;max-height:400px;object-fit:cover;display:block&quot;/>'">Your browser doesn't support video playback.</video><div style="position:absolute;bottom:12px;left:12px;display:flex;gap:8px;flex-wrap:wrap">${badgeHtml}</div></div>`;
200
+ }
201
+ else if (thumbnailUrl) {
202
+ html += `<div style="position:relative"><img src="${thumbnailUrl}" style="width:100%;max-height:400px;object-fit:cover;display:block" onerror="this.style.display='none'" /><div style="position:absolute;bottom:12px;left:12px;display:flex;gap:8px;flex-wrap:wrap">${badgeHtml}</div></div>`;
135
203
  }
136
204
  // Header
137
205
  html += `<div style="padding:16px 20px">`;
@@ -201,11 +269,14 @@ export function createMcpServer(makeClient) {
201
269
  "https://*.cdninstagram.com",
202
270
  "https://*.ytimg.com",
203
271
  "https://*.googlevideo.com",
272
+ "https://*.licdn.com",
273
+ "https://*.linkedin.com",
204
274
  ];
205
275
  const apiUrl = process.env.ORCHYN_API_URL || process.env.ORCHYN_BASE_URL;
206
276
  if (apiUrl && apiUrl.trim()) {
207
277
  domains.push(apiUrl.trim().replace(/\/+$/, ""));
208
278
  }
279
+ const domain = await computeAppDomain();
209
280
  return {
210
281
  contents: [
211
282
  {
@@ -214,9 +285,11 @@ export function createMcpServer(makeClient) {
214
285
  text: ORCHYN_UI_TEMPLATE,
215
286
  _meta: {
216
287
  ui: {
288
+ ...(domain ? { domain } : {}),
217
289
  csp: {
218
290
  resourceDomains: domains,
219
291
  },
292
+ prefersBorder: false,
220
293
  },
221
294
  },
222
295
  },
@@ -228,7 +301,7 @@ export function createMcpServer(makeClient) {
228
301
  description: "Analyze a social post (video, image, carousel/slideshow) from its link — " +
229
302
  "imports the media and runs AI analysis over the actual content (video frames, carousel images, caption). " +
230
303
  "Supports TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu and Bilibili. Returns the full analysis once finished. First use free per user.",
231
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
304
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
232
305
  inputSchema: z
233
306
  .object({
234
307
  url: z.string().describe("Public post URL (TikTok/Instagram/YouTube/X, Douyin, Xiaohongshu or Bilibili)."),
@@ -257,11 +330,16 @@ export function createMcpServer(makeClient) {
257
330
  const creatorHandle = String(post.creatorHandle ?? "");
258
331
  const title = String(post.title ?? post.caption ?? "");
259
332
  const htmlCard = buildAnalysisHtmlCard(proxied, thumbnailUrl, platform, creatorHandle, title);
260
- const images = (result.inlineImages ?? []).map((img) => ({
261
- type: "image",
262
- data: String(img.data),
263
- mimeType: String(img.mimeType ?? "image/jpeg"),
264
- }));
333
+ // The backend attaches `_inlineImages` as permanent orchyn public URLs
334
+ // (re-hosted into storage, HEIC already transcoded to JPEG). The SDK's
335
+ // ImageContent only accepts base64 `data`, so fetch each URL and encode it.
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 ?? [])
339
+ .filter((img) => typeof img === "object" && img !== null)
340
+ .map((img) => "url" in img && img.url
341
+ ? fetchAsBase64Image(String(img.url), 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);
265
343
  const textJson = JSON.stringify(proxied, null, 2);
266
344
  return {
267
345
  content: [...images, { type: "text", text: `${htmlCard}\n\n${textJson}` }],
@@ -284,7 +362,7 @@ export function createMcpServer(makeClient) {
284
362
  description: "Fetch a social post's media from a TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu or Bilibili URL: " +
285
363
  "contentType (video/image/carousel/slideshow), title, caption, author, stats and direct media URLs. " +
286
364
  "Returns an inline thumbnail image. Consumes 1 orchyn credit. First use free per user.",
287
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
365
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
288
366
  inputSchema: z
289
367
  .object({
290
368
  url: z.string().describe("Full public post URL."),
@@ -293,7 +371,7 @@ export function createMcpServer(makeClient) {
293
371
  }, async (args, extra) => {
294
372
  const client = await makeClient(extra);
295
373
  try {
296
- return toToolResult(await client.callTool("get_social_media", { url: args.url }));
374
+ return await toToolResult(await client.callTool("get_social_media", { url: args.url }));
297
375
  }
298
376
  catch (err) {
299
377
  return toolError("get_social_media failed", err);
@@ -304,7 +382,7 @@ export function createMcpServer(makeClient) {
304
382
  description: "Discover recent posts (video, image, carousel, slideshow) for a niche on YouTube, TikTok, Instagram, Douyin, Xiaohongshu, X/Twitter or Bilibili. " +
305
383
  "Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
306
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.',
307
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
385
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
308
386
  inputSchema: z
309
387
  .object({
310
388
  niche: z.string().describe("Niche/topic, e.g. 'fitness'."),
@@ -320,7 +398,7 @@ export function createMcpServer(makeClient) {
320
398
  }, async (args, extra) => {
321
399
  const client = await makeClient(extra);
322
400
  try {
323
- return toToolResult(await client.callTool("discover_social_posts", { ...args }));
401
+ return await toToolResult(await client.callTool("discover_social_posts", { ...args }));
324
402
  }
325
403
  catch (err) {
326
404
  return toolError("discover_social_posts failed", err);
@@ -328,15 +406,15 @@ export function createMcpServer(makeClient) {
328
406
  });
329
407
  server.registerTool("get_user_posts", {
330
408
  title: "Get User Posts",
331
- description: "List recent posts by a creator handle (e.g. @zoundsapp) on TikTok, Instagram, YouTube, Douyin, Xiaohongshu, X/Twitter or Bilibili. " +
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'). " +
332
410
  "Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
333
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.",
334
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
412
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
335
413
  inputSchema: z
336
414
  .object({
337
415
  username: z.string().describe("Creator handle, e.g. 'zoundsapp' or '@zoundsapp'."),
338
416
  platform: z
339
- .enum(["tiktok", "instagram", "youtube", "douyin", "xiaohongshu", "twitter", "bilibili"])
417
+ .enum(["tiktok", "instagram", "youtube", "douyin", "xiaohongshu", "twitter", "bilibili", "linkedin"])
340
418
  .optional()
341
419
  .describe("Which platform (default tiktok)."),
342
420
  limit: z.number().int().optional().describe("Max posts (default 6)."),
@@ -345,7 +423,7 @@ export function createMcpServer(makeClient) {
345
423
  }, async (args, extra) => {
346
424
  const client = await makeClient(extra);
347
425
  try {
348
- return toToolResult(await client.callTool("get_user_posts", { ...args }));
426
+ return await toToolResult(await client.callTool("get_user_posts", { ...args }));
349
427
  }
350
428
  catch (err) {
351
429
  return toolError("get_user_posts failed", err);
@@ -353,15 +431,15 @@ export function createMcpServer(makeClient) {
353
431
  });
354
432
  server.registerTool("analyze_creator_profile", {
355
433
  title: "Analyze Creator Profile",
356
- description: "Deep-dive a whole creator profile on TikTok, Instagram, YouTube, Douyin, Xiaohongshu, X/Twitter or Bilibili: fetch recent posts, run multimodal AI on up to 3, " +
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, " +
357
435
  "then synthesize a profile report — creator summary, niche, content themes, hook styles, strengths/weaknesses, " +
358
436
  "engagement patterns, audience insights, variation ideas, collaboration fit. Consumes 15 orchyn credits. First use free per user.",
359
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
437
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
360
438
  inputSchema: z
361
439
  .object({
362
440
  username: z.string().describe("Creator handle, e.g. 'zoundsapp'."),
363
441
  platform: z
364
- .enum(["tiktok", "instagram", "youtube", "douyin", "xiaohongshu", "twitter", "bilibili"])
442
+ .enum(["tiktok", "instagram", "youtube", "douyin", "xiaohongshu", "twitter", "bilibili", "linkedin"])
365
443
  .optional()
366
444
  .describe("Which platform (default tiktok)."),
367
445
  limit: z.number().int().optional().describe("Posts to fetch (default 6; first 3 analyzed)."),
@@ -371,7 +449,7 @@ export function createMcpServer(makeClient) {
371
449
  }, async (args, extra) => {
372
450
  const client = await makeClient(extra);
373
451
  try {
374
- return toToolResult(await client.callTool("analyze_creator_profile", { ...args }));
452
+ return await toToolResult(await client.callTool("analyze_creator_profile", { ...args }));
375
453
  }
376
454
  catch (err) {
377
455
  return toolError("analyze_creator_profile failed", err);
@@ -379,19 +457,19 @@ export function createMcpServer(makeClient) {
379
457
  });
380
458
  server.registerTool("get_post_comments", {
381
459
  title: "Get Post Comments",
382
- description: "Fetch top comments for a post URL on TikTok, Instagram, YouTube, Douyin, X/Twitter or Bilibili, plus keyword clusters from TikTok Analytics " +
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 " +
383
461
  "when available — audience sentiment/audience-signal analysis. Consumes 2 orchyn credits. First use free per user.",
384
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
462
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
385
463
  inputSchema: z
386
464
  .object({
387
- url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/Douyin/X/Bilibili)."),
465
+ url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/Douyin/X/Bilibili/LinkedIn)."),
388
466
  limit: z.number().int().optional().describe("Max comments (default 20)."),
389
467
  })
390
468
  .strict(),
391
469
  }, async (args, extra) => {
392
470
  const client = await makeClient(extra);
393
471
  try {
394
- return toToolResult(await client.callTool("get_post_comments", { ...args }));
472
+ return await toToolResult(await client.callTool("get_post_comments", { ...args }));
395
473
  }
396
474
  catch (err) {
397
475
  return toolError("get_post_comments failed", err);
@@ -401,7 +479,7 @@ export function createMcpServer(makeClient) {
401
479
  title: "Search Creators",
402
480
  description: "Search creators by niche/keyword on TikTok, Instagram, Xiaohongshu, YouTube or Douyin — username, nickname, follower count, " +
403
481
  "signature, verified status. Use to find influencers to vet or analyze. Consumes 2 orchyn credits. First use free per user.",
404
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
482
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
405
483
  inputSchema: z
406
484
  .object({
407
485
  keyword: z.string().describe("Niche/keyword, e.g. 'fitness' or a creator name."),
@@ -415,7 +493,7 @@ export function createMcpServer(makeClient) {
415
493
  }, async (args, extra) => {
416
494
  const client = await makeClient(extra);
417
495
  try {
418
- return toToolResult(await client.callTool("search_creators", { ...args }));
496
+ return await toToolResult(await client.callTool("search_creators", { ...args }));
419
497
  }
420
498
  catch (err) {
421
499
  return toolError("search_creators failed", err);
@@ -425,7 +503,7 @@ export function createMcpServer(makeClient) {
425
503
  title: "Get Similar Creators",
426
504
  description: "Find lookalike creators for a given handle — TikTok similar-user recommendations or Instagram " +
427
505
  "similar users. Useful for scaling: 'if this creator works, here are more like them'. Consumes 2 orchyn credits. First use free per user.",
428
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
506
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
429
507
  inputSchema: z
430
508
  .object({
431
509
  username: z.string().describe("Seed creator handle, e.g. 'zoundsapp'."),
@@ -435,7 +513,7 @@ export function createMcpServer(makeClient) {
435
513
  }, async (args, extra) => {
436
514
  const client = await makeClient(extra);
437
515
  try {
438
- return toToolResult(await client.callTool("get_similar_creators", { ...args }));
516
+ return await toToolResult(await client.callTool("get_similar_creators", { ...args }));
439
517
  }
440
518
  catch (err) {
441
519
  return toolError("get_similar_creators failed", err);
@@ -445,7 +523,7 @@ export function createMcpServer(makeClient) {
445
523
  title: "Discover Sounds",
446
524
  description: "Discover trending sounds/music for a keyword on TikTok or Instagram — the sound is a huge ranking " +
447
525
  "signal for TikTok virality. Returns title, artist, duration, play/cover URLs. Consumes 2 orchyn credits. First use free per user.",
448
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
526
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
449
527
  inputSchema: z
450
528
  .object({
451
529
  keyword: z.string().describe("Niche/keyword, e.g. 'gym'."),
@@ -456,7 +534,7 @@ export function createMcpServer(makeClient) {
456
534
  }, async (args, extra) => {
457
535
  const client = await makeClient(extra);
458
536
  try {
459
- return toToolResult(await client.callTool("discover_sounds", { ...args }));
537
+ return await toToolResult(await client.callTool("discover_sounds", { ...args }));
460
538
  }
461
539
  catch (err) {
462
540
  return toolError("discover_sounds failed", err);
@@ -465,12 +543,12 @@ export function createMcpServer(makeClient) {
465
543
  server.registerTool("check_orchyn_credits", {
466
544
  title: "Check Orchyn Credits",
467
545
  description: "Check your orchyn credit balance, billing URL and pack size. No cost — call anytime to see remaining credits before running other tools.",
468
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
546
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
469
547
  inputSchema: z.object({}).strict(),
470
548
  }, async (_args, extra) => {
471
549
  const client = await makeClient(extra);
472
550
  try {
473
- return toToolResult(await client.callTool("check_orchyn_credits", {}));
551
+ return await toToolResult(await client.callTool("check_orchyn_credits", {}));
474
552
  }
475
553
  catch (err) {
476
554
  return toolError("check_orchyn_credits failed", err);
@@ -479,12 +557,12 @@ export function createMcpServer(makeClient) {
479
557
  server.registerTool("buy_orchyn_credits", {
480
558
  title: "Buy Orchyn Credits",
481
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.",
482
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
560
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
483
561
  inputSchema: z.object({}).strict(),
484
562
  }, async (_args, extra) => {
485
563
  const client = await makeClient(extra);
486
564
  try {
487
- return toToolResult(await client.callTool("buy_orchyn_credits", {}));
565
+ return await toToolResult(await client.callTool("buy_orchyn_credits", {}));
488
566
  }
489
567
  catch (err) {
490
568
  return toolError("buy_orchyn_credits failed", err);
@@ -495,7 +573,7 @@ export function createMcpServer(makeClient) {
495
573
  description: "Import a social post URL AND understand it with multimodal AI over the actual video/images: " +
496
574
  "summary, hook strength, viral triggers, format breakdown and variation ideas. Includes the thumbnail. " +
497
575
  "Supports TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu and Bilibili. Consumes 6 orchyn credits. First use free per user.",
498
- _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
576
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, "ui/resourceUri": UI_RESOURCE_URI },
499
577
  inputSchema: z
500
578
  .object({
501
579
  url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/X/Douyin/Xiaohongshu/Bilibili)."),
@@ -508,7 +586,7 @@ export function createMcpServer(makeClient) {
508
586
  }, async (args, extra) => {
509
587
  const client = await makeClient(extra);
510
588
  try {
511
- return toToolResult(await client.callTool("understand_social_post", { ...args }));
589
+ return await toToolResult(await client.callTool("understand_social_post", { ...args }));
512
590
  }
513
591
  catch (err) {
514
592
  return toolError("understand_social_post failed", err);
@@ -75,7 +75,7 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
75
75
  .gallery{display:flex;flex-wrap:wrap;gap:10px;}
76
76
  .card{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;width:300px;background:var(--card);display:inline-block;vertical-align:top;box-shadow:var(--shadow-sm);transition:var(--transition);cursor:default;}
77
77
  .card:hover{box-shadow:var(--shadow-md);transform:translateY(-2px);border-color:var(--muted);}
78
- .card-wide{max-width:520px;width:100%;}
78
+ .card-wide{width:100%;}
79
79
  .card img{width:100%;height:200px;object-fit:cover;display:block;transition:transform .3s ease;}
80
80
  .card:hover img{transform:scale(1.02);}
81
81
  .card-wide img{max-height:340px;}
@@ -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:"";
@@ -221,10 +222,22 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
221
222
  }
222
223
  });
223
224
 
224
- // Show skeleton after 300ms of waiting (feels instant but shows loading for slow tools)
225
+ // Show loading state after 300ms branded spinner until tool name arrives,
226
+ // then switch to the correct skeleton type.
225
227
  loadingTimer=setTimeout(function(){
226
228
  var app=document.getElementById("app");
227
- if(app)app.innerHTML=renderSkeleton(currentTool);
229
+ if(!app)return;
230
+ if(currentTool){
231
+ // Tool name already known → show correct skeleton
232
+ app.innerHTML=renderSkeleton(currentTool);
233
+ }else{
234
+ // Tool name not yet known → show branded spinner (neutral)
235
+ app.innerHTML='<div class="loading-container fade-in">'
236
+ +'<div class="orchyn-logo"><svg width="40" height="40" viewBox="0 0 48 48" fill="none"><g fill="var(--accent)" transform="translate(24 24)"><circle r="4.1"/><g id="rl"><path d="M-2.85 -5.2 L0 -20.6 L2.85 -5.2 L1.15 1.1 L-1.15 1.1 Z"/></g><use href="#rl" transform="rotate(45)"/><use href="#rl" transform="rotate(90)"/><use href="#rl" transform="rotate(135)"/><use href="#rl" transform="rotate(180)"/><use href="#rl" transform="rotate(225)"/><use href="#rl" transform="rotate(270)"/><use href="#rl" transform="rotate(315)"/></g></svg></div>'
237
+ +'<div class="loading-brand">Orchyn</div>'
238
+ +'<div class="loading-text">Loading…</div>'
239
+ +'</div>';
240
+ }
228
241
  },300);
229
242
 
230
243
  // Fallback: if no tool-result arrives within 3s, check if data is embedded
@@ -257,20 +270,63 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
257
270
  +'</div></div>';
258
271
  },3000);
259
272
 
260
- // 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
+ }
261
286
  send("ui/initialize",{
262
287
  protocolVersion:"2026-01-26",
288
+ appInfo:{name:"orchyn-view",version:"2.0.0"},
289
+ appCapabilities:{availableDisplayModes:["inline"]},
263
290
  capabilities:{},
264
291
  clientInfo:{name:"orchyn-view",version:"2.0.0"}
265
- }).then(function(){
266
- window.parent.postMessage({jsonrpc:"2.0",method:"ui/notifications/initialized",params:{}},"*");
267
- }).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);
268
307
 
269
308
  // ─── Helpers ───
270
309
  function esc(s){return String(s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}
271
310
  function fmtNum(n){n=Number(n)||0;return n>=1e6?(n/1e6).toFixed(1)+"M":n>=1e3?(n/1e3).toFixed(1)+"K":String(n);}
272
- function pColor(p){return{tiktok:"#000",instagram:"#E1306C",youtube:"#FF0000",douyin:"#000",xiaohongshu:"#FF2442",twitter:"#1DA1F2",x:"#1DA1F2",bilibili:"#00A1D6"}[p]||"#6B7280";}
273
- function pEmoji(p){return{tiktok:"🎵",instagram:"📸",youtube:"▶️",douyin:"🎶",xiaohongshu:"📕",twitter:"🐦",x:"🐦",bilibili:"📺"}[p]||"🔗";}
311
+ function pColor(p){return{tiktok:"#000",douyin:"#000",instagram:"#E4405F",youtube:"#FF0000",xiaohongshu:"#FF2442",x:"#000",twitter:"#1DA1F2",bilibili:"#00A1D6",linkedin:"#0A66C2"}[p]||"#6B7280";}
312
+ // Official brand mark (simple-icons path) as an inline SVG that inherits
313
+ // the current text color via fill="currentColor" — matches badge accents.
314
+ function pSvg(p,size){
315
+ var s=size||14,paths={
316
+ tiktok:"M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z",
317
+ instagram:"M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077",
318
+ youtube:"M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z",
319
+ x:"M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z",
320
+ twitter:"M21.543 7.104c.015.211.015.423.015.636 0 6.507-4.954 14.01-14.01 14.01v-.003A13.94 13.94 0 0 1 0 19.539a9.88 9.88 0 0 0 7.287-2.041 4.93 4.93 0 0 1-4.6-3.42 4.916 4.916 0 0 0 2.223-.084A4.926 4.926 0 0 1 .96 9.167v-.062a4.887 4.887 0 0 0 2.235.616A4.928 4.928 0 0 1 1.67 3.148 13.98 13.98 0 0 0 11.82 8.292a4.929 4.929 0 0 1 8.39-4.49 9.868 9.868 0 0 0 3.128-1.196 4.941 4.941 0 0 1-2.165 2.724A9.828 9.828 0 0 0 24 4.555a10.019 10.019 0 0 1-2.457 2.549z",
321
+ douyin:"M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z",
322
+ xiaohongshu:"M22.405 9.879c.002.016.01.02.07.019h.725a.797.797 0 0 0 .78-.972.794.794 0 0 0-.884-.618.795.795 0 0 0-.692.794c0 .101-.002.666.001.777zm-11.509 4.808c-.203.001-1.353.004-1.685.003a2.528 2.528 0 0 1-.766-.126.025.025 0 0 0-.03.014L7.7 16.127a.025.025 0 0 0 .01.032c.111.06.336.124.495.124.66.01 1.32.002 1.981 0 .01 0 .02-.006.023-.015l.712-1.545a.025.025 0 0 0-.024-.036zM.477 9.91c-.071 0-.076.002-.076.01a.834.834 0 0 0-.01.08c-.027.397-.038.495-.234 3.06-.012.24-.034.389-.135.607-.026.057-.033.042.003.112.046.092.681 1.523.787 1.74.008.015.011.02.017.02.008 0 .033-.026.047-.044.147-.187.268-.391.371-.606.306-.635.44-1.325.486-1.706.014-.11.021-.22.03-.33l.204-2.616.022-.293c.003-.029 0-.033-.03-.034zm7.203 3.757a1.427 1.427 0 0 1-.135-.607c-.004-.084-.031-.39-.235-3.06a.443.443 0 0 0-.01-.082c-.004-.011-.052-.008-.076-.008h-1.48c-.03.001-.034.005-.03.034l.021.293c.076.982.153 1.964.233 2.946.05.4.186 1.085.487 1.706.103.215.223.419.37.606.015.018.037.051.048.049.02-.003.742-1.642.804-1.765.036-.07.03-.055.003-.112zm3.861-.913h-.872a.126.126 0 0 1-.116-.178l1.178-2.625a.025.025 0 0 0-.023-.035l-1.318-.003a.148.148 0 0 1-.135-.21l.876-1.954a.025.025 0 0 0-.023-.035h-1.56c-.01 0-.02.006-.024.015l-.926 2.068c-.085.169-.314.634-.399.938a.534.534 0 0 0-.02.191.46.46 0 0 0 .23.378.981.981 0 0 0 .46.119h.59c.041 0-.688 1.482-.834 1.972a.53.53 0 0 0-.023.172.465.465 0 0 0 .23.398c.15.092.342.12.475.12l1.66-.001c.01 0 .02-.006.023-.015l.575-1.28a.025.025 0 0 0-.024-.035zm-6.93-4.937H3.1a.032.032 0 0 0-.034.033c0 1.048-.01 2.795-.01 6.829 0 .288-.269.262-.28.262h-.74c-.04.001-.044.004-.04.047.001.037.465 1.064.555 1.263.01.02.03.033.051.033.157.003.767.009.938-.014.153-.02.3-.06.438-.132.3-.156.49-.419.595-.765.052-.172.075-.353.075-.533.002-2.33 0-4.66-.007-6.991a.032.032 0 0 0-.032-.032zm11.784 6.896c0-.014-.01-.021-.024-.022h-1.465c-.048-.001-.049-.002-.05-.049v-4.66c0-.072-.005-.07.07-.07h.863c.08 0 .075.004.075-.074V8.393c0-.082.006-.076-.08-.076h-3.5c-.064 0-.075-.006-.075.073v1.445c0 .083-.006.077.08.077h.854c.075 0 .07-.004.07.07v4.624c0 .095.008.084-.085.084-.37 0-1.11-.002-1.304 0-.048.001-.06.03-.06.03l-.697 1.519s-.014.025-.008.036c.006.01.013.008.058.008 1.748.003 3.495.002 5.243.002.03-.001.034-.006.035-.033v-1.539zm4.177-3.43c0 .013-.007.023-.02.024-.346.006-.692.004-1.037.004-.014-.002-.022-.01-.022-.024-.005-.434-.007-.869-.01-1.303 0-.072-.006-.071.07-.07l.733-.003c.041 0 .081.002.12.015.093.025.16.107.165.204.006.431.002 1.153.001 1.153zm2.67.244a1.953 1.953 0 0 0-.883-.222h-.18c-.04-.001-.04-.003-.042-.04V10.21c0-.132-.007-.263-.025-.394a1.823 1.823 0 0 0-.153-.53 1.533 1.533 0 0 0-.677-.71 2.167 2.167 0 0 0-1-.258c-.153-.003-.567 0-.72 0-.07 0-.068.004-.068-.065V7.76c0-.031-.01-.041-.046-.039H17.93s-.016 0-.023.007c-.006.006-.008.012-.008.023v.546c-.008.036-.057.015-.082.022h-.95c-.022.002-.028.008-.03.032v1.481c0 .09-.004.082.082.082h.913c.082 0 .072.128.072.128V11.19s.003.117-.06.117h-1.482c-.068 0-.06.082-.06.082v1.445s-.01.068.064.068h1.457c.082 0 .076-.006.076.079v3.225c0 .088-.007.081.082.081h1.43c.09 0 .082.007.082-.08v-3.27c0-.029.006-.035.033-.035l2.323-.003c.098 0 .191.02.28.061a.46.46 0 0 1 .274.407c.008.395.003.79.003 1.185 0 .259-.107.367-.33.367h-1.218c-.023.002-.029.008-.028.033.184.437.374.871.57 1.303a.045.045 0 0 0 .04.026c.17.005.34.002.51.003.15-.002.517.004.666-.01a2.03 2.03 0 0 0 .408-.075c.59-.18.975-.698.976-1.313v-1.981c0-.128-.01-.254-.034-.38 0 .078-.029-.641-.724-.998z",
323
+ bilibili:"M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z",
324
+ linkedin:"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z",
325
+ unknown:"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"
326
+ };
327
+ var d=paths[p]||paths.unknown;
328
+ return '<svg viewBox="0 0 24 24" width="'+s+'" height="'+s+'" fill="currentColor" style="vertical-align:-2px;flex-shrink:0;display:inline-block" aria-hidden="true"><path d="'+d+'"/></svg>';
329
+ }
274
330
 
275
331
  function copyText(text){
276
332
  navigator.clipboard.writeText(text).catch(function(){});
@@ -291,7 +347,7 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
291
347
  +'<span style="font-size:13px;font-weight:600;color:var(--muted)">Orchyn</span></div>';
292
348
  // Single URL tools → 1 wide card skeleton
293
349
  if(SINGLE_URL_TOOLS[toolName]){
294
- return brandHeader+'<div class="fade-in" style="max-width:520px">'
350
+ return brandHeader+'<div class="fade-in">'
295
351
  +'<div class="skeleton-card" style="width:100%">'
296
352
  +'<div class="skeleton skeleton-thumb" style="height:220px"></div>'
297
353
  +'<div style="padding:14px 16px">'
@@ -313,7 +369,21 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
313
369
  +'<div class="skeleton" style="width:140px;height:14px"></div>'
314
370
  +'</div></div>';
315
371
  }
316
- // Gallery tools → multiple small card skeletons (default)
372
+ // Gallery tools → multiple small card skeletons
373
+ // Unknown tool → default to 1 wide card (most common case: single URL tools)
374
+ if(!toolName||SINGLE_URL_TOOLS[toolName]||CREDIT_TOOLS[toolName]){
375
+ // Already handled above, but catch unknown tools with single card
376
+ if(!toolName){
377
+ return brandHeader+'<div class="fade-in" style="max-width:520px">'
378
+ +'<div class="skeleton-card" style="width:100%">'
379
+ +'<div class="skeleton skeleton-thumb" style="height:220px"></div>'
380
+ +'<div style="padding:14px 16px">'
381
+ +'<div class="skeleton skeleton-badge"></div>'
382
+ +'<div class="skeleton skeleton-line" style="width:90%"></div>'
383
+ +'<div class="skeleton skeleton-line medium"></div>'
384
+ +'</div></div></div>';
385
+ }
386
+ }
317
387
  var count=toolName==='discover_sounds'?4:toolName==='search_creators'||toolName==='get_similar_creators'?3:6;
318
388
  return brandHeader+'<div class="skeleton-gallery stagger">'
319
389
  +Array(count).fill(0).map(function(){
@@ -328,7 +398,7 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
328
398
 
329
399
  // ─── Post Card ───
330
400
  function postCard(p,wide){
331
- var platform=p.platform||"unknown",color=pColor(platform),emoji=pEmoji(platform);
401
+ var platform=p.platform||"unknown",color=pColor(platform),brandSvg=pSvg(platform);
332
402
  var title=p.title||p.caption||"",handle=p.creatorHandle||"",url=p.externalUrl||"";
333
403
  var thumb=p.thumbnailUrl||"",ct=p.contentType||"post";
334
404
  var views=p.views||0,likes=p.likes||0,comments=p.comments||0,shares=p.shares||0;
@@ -338,11 +408,42 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
338
408
  .map(function(s){return'<span class="stat-pill">'+s[1]+" "+fmtNum(s[0])+"</span>";}).join("");
339
409
  var handleHtml=handle?'<span class="handle">@'+esc(handle)+"</span>":"";
340
410
  var linkHtml=url?'<a href="'+esc(url)+'" target="_blank" class="btn" style="background:'+color+'">View on '+esc(platform)+' →</a>':"";
341
- var thumbHtml=thumb?'<img src="'+esc(thumb)+'" alt="thumbnail" loading="lazy"/>':"";
411
+ // Prefer video playback over a static thumbnail when the post has one
412
+ // (videoUrl, or a video mediaItem preview_url). Videos are proxied /
413
+ // re-hosted to a permanent orchyn URL so they play inside the CSP.
414
+ var video="",images=[];
415
+ if(typeof p.videoUrl==="string"&&p.videoUrl)video=p.videoUrl;
416
+ if(p.mediaItems&&p.mediaItems.length){
417
+ for(var i=0;i<p.mediaItems.length;i++){
418
+ var m=p.mediaItems[i];
419
+ if(m.kind==="video"&&m.preview_url){if(!video)video=m.preview_url;}
420
+ else if(m.kind==="image"&&m.preview_url&&images.length<8)images.push(m.preview_url);
421
+ }
422
+ }
423
+ var mediaHtml="";
424
+ var vh=wide?"340px":"200px";
425
+ var bodyText=p.caption||p.text||"";
426
+ if(video){
427
+ mediaHtml='<video controls preload="metadata" playsinline poster="'+esc(thumb)+'" style="width:100%;height:'+vh+';object-fit:cover;display:block;background:#000">'
428
+ +'Your browser doesn\'t support video playback.</video>';
429
+ }else if(images.length>1){
430
+ // Carousel / multi-image gallery — horizontally scrollable strip.
431
+ var strip=images.map(function(u,i){
432
+ return '<img src="'+esc(u)+'" alt="slide '+(i+1)+'" loading="lazy" style="height:'+vh+';max-width:85%;object-fit:cover;border-radius:6px;flex-shrink:0;display:block"/>';
433
+ }).join("");
434
+ mediaHtml='<div style="display:flex;gap:6px;overflow-x:auto;padding:8px;background:var(--border);border-bottom:1px solid var(--border)">'+strip+'</div>'
435
+ +'<div style="display:flex;align-items:center;gap:6px;padding:6px 10px;font-size:11px;color:var(--muted);font-weight:600;border-bottom:1px solid var(--border)">🖼 '+images.length+' images</div>';
436
+ }else if(thumb){
437
+ mediaHtml='<img src="'+esc(thumb)+'" alt="thumbnail" loading="lazy"/>';
438
+ }else if(bodyText){
439
+ // Text-only post (LinkedIn / X) — styled quote block.
440
+ mediaHtml='<div style="padding:16px 18px;border-bottom:1px solid var(--border);background:var(--card)">'
441
+ +'<div style="font-size:13px;line-height:1.55;color:var(--fg);white-space:pre-line">'+esc(bodyText.length>400?bodyText.slice(0,400)+"…":bodyText)+'</div></div>';
442
+ }
342
443
  var ctTag=wide?'<span style="font-size:12px;color:var(--muted);text-transform:capitalize">'+esc(ct)+"</span>":"";
343
- return '<div class="'+cls+'">'+thumbHtml+'<div class="card-body">'
444
+ return '<div class="'+cls+'">'+mediaHtml+'<div class="card-body">'
344
445
  +'<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px">'
345
- +'<span class="badge" style="background:'+color+'15;color:'+color+'">'+emoji+" "+esc(platform)+"</span>"
446
+ +'<span class="badge" style="background:'+color+'15;color:'+color+';display:inline-flex;align-items:center;gap:4px">'+brandSvg+" "+esc(platform)+"</span>"
346
447
  +ctTag+handleHtml+"</div>"
347
448
  +'<div class="title">'+esc(title.length>160?title.slice(0,160)+"…":title)+"</div>"
348
449
  +'<div class="stats">'+statsHtml+"</div>"
@@ -351,13 +452,13 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
351
452
 
352
453
  // ─── Creator Card ───
353
454
  function creatorCard(c){
354
- var platform=c.platform||"tiktok",color=pColor(platform),emoji=pEmoji(platform);
455
+ var platform=c.platform||"tiktok",color=pColor(platform),brandSvg=pSvg(platform,22);
355
456
  var username=c.username||c.uniqueId||"",nickname=c.nickname||c.displayName||username;
356
457
  var followers=c.followers||c.followerCount||0,sig=(c.signature||c.bio||"").slice(0,100);
357
458
  var verified=c.verified||false,avatar=c.avatarUrl||c.avatar_thumb||"";
358
459
  var avatarHtml=avatar
359
460
  ?'<img class="avatar" src="'+esc(avatar)+'" alt="avatar" loading="lazy"/>'
360
- :'<div class="avatar-placeholder" style="background:'+color+'20">'+emoji+"</div>";
461
+ :'<div class="avatar-placeholder" style="background:'+color+'20">'+brandSvg+"</div>";
361
462
  var vBadge=verified?'<span class="verified" style="background:'+color+'">✓</span>':"";
362
463
  return '<div class="creator-card">'
363
464
  +'<div style="display:flex;align-items:center;gap:12px;margin-bottom:10px">'
@@ -489,29 +590,25 @@ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--
489
590
  if(d.post||d.platform){app.innerHTML=postCard(d.post||d,true);return;}
490
591
  // Fallback
491
592
  app.innerHTML='<div class="json-block fade-in">'+esc(JSON.stringify(d,null,2))+"</div>";
593
+ setTimeout(reportSize,50);
492
594
  }
493
595
 
494
596
  function updateProgress(params){
495
597
  var app=document.getElementById("app");
496
598
  if(!app)return;
497
- // If we just learned the tool name and the current skeleton is wrong,
498
- // re-render with the correct skeleton type
599
+ // Learn the tool name
499
600
  var toolName=params&&params.name?params.name:currentTool;
500
601
  if(toolName&&toolName!==currentTool){
501
602
  currentTool=toolName;
502
- // Only re-render skeleton if we're still in loading state (no result yet)
503
- var el=document.getElementById("app");
504
- if(el&&(el.querySelector(".loading-spinner")||el.querySelector(".skeleton"))){
505
- el.innerHTML=renderSkeleton(currentTool);
603
+ // Switch from spinner to correct skeleton if still loading
604
+ if(app.querySelector(".loading-spinner")||app.querySelector(".loading-container")||app.querySelector(".skeleton")){
605
+ app.innerHTML=renderSkeleton(currentTool);
506
606
  }
507
607
  }
508
- var text=params&&params.arguments?JSON.stringify(params.arguments):"Processing…";
509
- app.innerHTML='<div class="loading-container fade-in">'
510
- +'<div class="orchyn-logo"><svg width="36" height="36" viewBox="0 0 48 48" fill="none"><g fill="var(--accent)" transform="translate(24 24)"><circle r="4.1"/><g id="rp"><path d="M-2.85 -5.2 L0 -20.6 L2.85 -5.2 L1.15 1.1 L-1.15 1.1 Z"/></g><use href="#rp" transform="rotate(45)"/><use href="#rp" transform="rotate(90)"/><use href="#rp" transform="rotate(135)"/><use href="#rp" transform="rotate(180)"/><use href="#rp" transform="rotate(225)"/><use href="#rp" transform="rotate(270)"/><use href="#rp" transform="rotate(315)"/></g></svg></div>'
511
- +'<div class="loading-brand">Orchyn</div>'
512
- +'<div class="loading-text">'+esc(text.length>80?text.slice(0,80)+"…":text)+'</div>'
513
- +'<div class="progress-bar" style="width:200px"><div class="progress-fill" style="width:60%"></div></div>'
514
- +'</div>';
608
+ // Add a progress bar below the existing content if not already present
609
+ if(!app.querySelector(".progress-bar")){
610
+ app.innerHTML=app.innerHTML+'<div style="text-align:center;margin-top:12px"><div class="progress-bar" style="width:200px;margin:0 auto"><div class="progress-fill" style="width:60%"></div></div></div>';
611
+ }
515
612
  }
516
613
 
517
614
  // Global helpers
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orchyn/mcp",
3
- "version": "1.12.3",
4
- "description": "MCP server for orchyn - fetch, discover and understand TikTok/Instagram/YouTube/X posts with AI (media metadata, niche discovery, hook & viral analysis)",
3
+ "version": "1.14.0",
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",
7
7
  "files": [