@orchyn/mcp 1.8.0 → 1.9.1

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.
@@ -9,6 +9,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
  import { z } from "zod";
10
10
  import { OrchynError } from "./orchyn.js";
11
11
  import { formatPaywallError, runVideoAnalysis, validatePostUrl } from "./video.js";
12
+ import { ORCHYN_UI_TEMPLATE } from "./ui-template.js";
13
+ /** MCP Apps extension identifier */
14
+ const UI_EXTENSION = "io.modelcontextprotocol/ui";
15
+ /** MIME type for MCP Apps HTML resources */
16
+ const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
17
+ /** Single UI resource URI shared by all tools */
18
+ const UI_RESOURCE_URI = "ui://orchyn/view";
12
19
  function toToolResult(proxy) {
13
20
  const images = proxy.contentBlocks
14
21
  .filter((c) => c.type === "image")
@@ -35,15 +42,39 @@ function toolError(prefix, err) {
35
42
  return { content: [{ type: "text", text: `${prefix}: ${msg}` }], isError: true };
36
43
  }
37
44
  export function createMcpServer(makeClient) {
38
- const server = new McpServer({
39
- name: "orchyn-mcp",
40
- version: "1.5.0",
45
+ const server = new McpServer({ name: "orchyn-mcp", version: "1.8.0" }, {
46
+ capabilities: {
47
+ resources: {},
48
+ extensions: {
49
+ [UI_EXTENSION]: { mimeTypes: [RESOURCE_MIME_TYPE] },
50
+ },
51
+ },
41
52
  });
53
+ // Register the single UI resource that all tools share.
54
+ // The host reads this resource, renders it in a sandboxed iframe,
55
+ // and pushes tool results via ui/notifications/tool-result.
56
+ server.registerResource("Orchyn Interactive View", UI_RESOURCE_URI, { mimeType: RESOURCE_MIME_TYPE }, async () => ({
57
+ contents: [
58
+ {
59
+ uri: UI_RESOURCE_URI,
60
+ mimeType: RESOURCE_MIME_TYPE,
61
+ text: ORCHYN_UI_TEMPLATE,
62
+ _meta: {
63
+ ui: {
64
+ csp: {
65
+ resourceDomains: ["https://*.tiktokcdn.com", "https://*.cdninstagram.com", "https://*.ytimg.com", "https://*.googlevideo.com"],
66
+ },
67
+ },
68
+ },
69
+ },
70
+ ],
71
+ }));
42
72
  server.registerTool("analyze_post", {
43
73
  title: "Analyze Post",
44
74
  description: "Analyze a social post (video, image, carousel/slideshow) from its link — " +
45
75
  "imports the media and runs AI analysis over the actual content (video frames, carousel images, caption). " +
46
76
  "Supports TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu and Bilibili. Returns the full analysis once finished. First use free per user.",
77
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
47
78
  inputSchema: z
48
79
  .object({
49
80
  url: z.string().describe("Public post URL (TikTok/Instagram/YouTube/X, Douyin, Xiaohongshu or Bilibili)."),
@@ -87,6 +118,7 @@ export function createMcpServer(makeClient) {
87
118
  description: "Fetch a social post's media from a TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu or Bilibili URL: " +
88
119
  "contentType (video/image/carousel/slideshow), title, caption, author, stats and direct media URLs. " +
89
120
  "Returns an inline thumbnail image. Consumes 1 orchyn credit. First use free per user.",
121
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
90
122
  inputSchema: z
91
123
  .object({
92
124
  url: z.string().describe("Full public post URL."),
@@ -106,6 +138,7 @@ export function createMcpServer(makeClient) {
106
138
  description: "Discover recent posts (video, image, carousel, slideshow) for a niche on YouTube, TikTok, Instagram, Douyin, Xiaohongshu, X/Twitter or Bilibili. " +
107
139
  "Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
108
140
  'Say "next" to paginate (offset), or "analyze the 2nd one" / "analyze all" for batch analysis. Consumes 2 orchyn credits. First use free per user.',
141
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
109
142
  inputSchema: z
110
143
  .object({
111
144
  niche: z.string().describe("Niche/topic, e.g. 'fitness'."),
@@ -132,6 +165,7 @@ export function createMcpServer(makeClient) {
132
165
  description: "List recent posts by a creator handle (e.g. @zoundsapp) on TikTok, Instagram, YouTube, Douyin, Xiaohongshu, X/Twitter or Bilibili. " +
133
166
  "Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
134
167
  "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.",
168
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
135
169
  inputSchema: z
136
170
  .object({
137
171
  username: z.string().describe("Creator handle, e.g. 'zoundsapp' or '@zoundsapp'."),
@@ -156,6 +190,7 @@ export function createMcpServer(makeClient) {
156
190
  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, " +
157
191
  "then synthesize a profile report — creator summary, niche, content themes, hook styles, strengths/weaknesses, " +
158
192
  "engagement patterns, audience insights, variation ideas, collaboration fit. Consumes 15 orchyn credits. First use free per user.",
193
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
159
194
  inputSchema: z
160
195
  .object({
161
196
  username: z.string().describe("Creator handle, e.g. 'zoundsapp'."),
@@ -180,6 +215,7 @@ export function createMcpServer(makeClient) {
180
215
  title: "Get Post Comments",
181
216
  description: "Fetch top comments for a post URL on TikTok, Instagram, YouTube, Douyin, X/Twitter or Bilibili, plus keyword clusters from TikTok Analytics " +
182
217
  "when available — audience sentiment/audience-signal analysis. Consumes 2 orchyn credits. First use free per user.",
218
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
183
219
  inputSchema: z
184
220
  .object({
185
221
  url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/Douyin/X/Bilibili)."),
@@ -199,6 +235,7 @@ export function createMcpServer(makeClient) {
199
235
  title: "Search Creators",
200
236
  description: "Search creators by niche/keyword on TikTok, Instagram, Xiaohongshu, YouTube or Douyin — username, nickname, follower count, " +
201
237
  "signature, verified status. Use to find influencers to vet or analyze. Consumes 2 orchyn credits. First use free per user.",
238
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
202
239
  inputSchema: z
203
240
  .object({
204
241
  keyword: z.string().describe("Niche/keyword, e.g. 'fitness' or a creator name."),
@@ -222,6 +259,7 @@ export function createMcpServer(makeClient) {
222
259
  title: "Get Similar Creators",
223
260
  description: "Find lookalike creators for a given handle — TikTok similar-user recommendations or Instagram " +
224
261
  "similar users. Useful for scaling: 'if this creator works, here are more like them'. Consumes 2 orchyn credits. First use free per user.",
262
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
225
263
  inputSchema: z
226
264
  .object({
227
265
  username: z.string().describe("Seed creator handle, e.g. 'zoundsapp'."),
@@ -241,6 +279,7 @@ export function createMcpServer(makeClient) {
241
279
  title: "Discover Sounds",
242
280
  description: "Discover trending sounds/music for a keyword on TikTok or Instagram — the sound is a huge ranking " +
243
281
  "signal for TikTok virality. Returns title, artist, duration, play/cover URLs. Consumes 2 orchyn credits. First use free per user.",
282
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
244
283
  inputSchema: z
245
284
  .object({
246
285
  keyword: z.string().describe("Niche/keyword, e.g. 'gym'."),
@@ -260,6 +299,7 @@ export function createMcpServer(makeClient) {
260
299
  server.registerTool("check_orchyn_credits", {
261
300
  title: "Check Orchyn Credits",
262
301
  description: "Check your orchyn credit balance, billing URL and pack size. No cost — call anytime to see remaining credits before running other tools.",
302
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
263
303
  inputSchema: z.object({}).strict(),
264
304
  }, async (_args, extra) => {
265
305
  const client = await makeClient(extra);
@@ -273,6 +313,7 @@ export function createMcpServer(makeClient) {
273
313
  server.registerTool("buy_orchyn_credits", {
274
314
  title: "Buy Orchyn Credits",
275
315
  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.",
316
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
276
317
  inputSchema: z.object({}).strict(),
277
318
  }, async (_args, extra) => {
278
319
  const client = await makeClient(extra);
@@ -288,6 +329,7 @@ export function createMcpServer(makeClient) {
288
329
  description: "Import a social post URL AND understand it with multimodal AI over the actual video/images: " +
289
330
  "summary, hook strength, viral triggers, format breakdown and variation ideas. Includes the thumbnail. " +
290
331
  "Supports TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu and Bilibili. Consumes 6 orchyn credits. First use free per user.",
332
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
291
333
  inputSchema: z
292
334
  .object({
293
335
  url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/X/Douyin/Xiaohongshu/Bilibili)."),
@@ -0,0 +1,425 @@
1
+ /**
2
+ * MCP Apps HTML UI template for Orchyn tools.
3
+ *
4
+ * Features:
5
+ * - Skeleton loading shimmer while tool executes
6
+ * - Smooth fade-in transitions when data arrives
7
+ * - Expandable analysis sections (click to expand/collapse)
8
+ * - Click-to-copy on hashtags, hooks, quotes
9
+ * - Hover micro-interactions on cards
10
+ * - Dark/light theme auto-detection
11
+ * - Progress indicator for multi-step tools
12
+ * - Responsive layout
13
+ */
14
+ export const ORCHYN_UI_TEMPLATE = `<!DOCTYPE html>
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="UTF-8"/>
18
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
19
+ <title>Orchyn</title>
20
+ <style>
21
+ :root{
22
+ --bg:#fafbfc;--fg:#111827;--muted:#6b7280;--border:#e5e7eb;
23
+ --card:#fff;--card-hover:#f9fafb;--tag:#f3f4f6;--accent:#14151a;
24
+ --shadow-sm:0 1px 2px rgba(0,0,0,.05);--shadow-md:0 4px 12px rgba(0,0,0,.08);
25
+ --shadow-lg:0 8px 24px rgba(0,0,0,.12);--radius:12px;--radius-sm:8px;
26
+ --transition:all .25s cubic-bezier(.4,0,.2,1);
27
+ --green:#10b981;--amber:#f59e0b;--red:#ef4444;--blue:#3b82f6;
28
+ }
29
+ @media(prefers-color-scheme:dark){:root{
30
+ --bg:#0f172a;--fg:#f1f5f9;--muted:#94a3b8;--border:#1e293b;
31
+ --card:#1e293b;--card-hover:#273548;--tag:#1e293b;--accent:#e2e8f0;
32
+ --shadow-sm:0 1px 2px rgba(0,0,0,.2);--shadow-md:0 4px 12px rgba(0,0,0,.3);
33
+ --shadow-lg:0 8px 24px rgba(0,0,0,.4);
34
+ }}
35
+ *{margin:0;padding:0;box-sizing:border-box;}
36
+ body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--bg);color:var(--fg);padding:16px;line-height:1.5;}
37
+
38
+ /* ─── Loading / Skeleton ─── */
39
+ .loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px;gap:16px;}
40
+ .loading-spinner{width:32px;height:32px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;}
41
+ @keyframes spin{to{transform:rotate(360deg)}}
42
+ .loading-text{font-size:14px;color:var(--muted);animation:pulse 1.5s ease-in-out infinite;}
43
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
44
+
45
+ .skeleton{background:var(--tag);border-radius:var(--radius-sm);overflow:hidden;position:relative;}
46
+ .skeleton::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.08),transparent);animation:shimmer 1.5s infinite;}
47
+ @keyframes shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
48
+ .skeleton-thumb{width:100%;height:180px;border-radius:var(--radius) var(--radius) 0 0;}
49
+ .skeleton-line{height:14px;margin:8px 14px;border-radius:4px;}
50
+ .skeleton-line.short{width:60%;}
51
+ .skeleton-line.medium{width:80%;}
52
+ .skeleton-badge{width:80px;height:20px;border-radius:999px;margin:12px 14px 0;}
53
+ .skeleton-gallery{display:flex;flex-wrap:wrap;gap:10px;}
54
+ .skeleton-card{width:300px;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;background:var(--card);}
55
+ .skeleton-creator{width:220px;border:1px solid var(--border);border-radius:var(--radius);padding:14px;background:var(--card);}
56
+ .skeleton-avatar{width:48px;height:48px;border-radius:50%;margin-bottom:8px;}
57
+
58
+ /* ─── Content entrance ─── */
59
+ .fade-in{animation:fadeIn .4s ease-out;}
60
+ @keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
61
+ .stagger>*{opacity:0;animation:fadeIn .4s ease-out forwards;}
62
+ .stagger>*:nth-child(1){animation-delay:.05s}
63
+ .stagger>*:nth-child(2){animation-delay:.1s}
64
+ .stagger>*:nth-child(3){animation-delay:.15s}
65
+ .stagger>*:nth-child(4){animation-delay:.2s}
66
+ .stagger>*:nth-child(5){animation-delay:.25s}
67
+ .stagger>*:nth-child(6){animation-delay:.3s}
68
+ .stagger>*:nth-child(7){animation-delay:.35s}
69
+ .stagger>*:nth-child(8){animation-delay:.4s}
70
+
71
+ /* ─── Cards ─── */
72
+ .gallery{display:flex;flex-wrap:wrap;gap:10px;}
73
+ .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;}
74
+ .card:hover{box-shadow:var(--shadow-md);transform:translateY(-2px);border-color:var(--muted);}
75
+ .card-wide{max-width:520px;width:100%;}
76
+ .card img{width:100%;height:200px;object-fit:cover;display:block;transition:transform .3s ease;}
77
+ .card:hover img{transform:scale(1.02);}
78
+ .card-wide img{max-height:340px;}
79
+ .card-body{padding:14px 16px;}
80
+
81
+ /* ─── Badges & Tags ─── */
82
+ .badge{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:999px;font-size:11px;font-weight:700;text-transform:capitalize;transition:var(--transition);}
83
+ .handle{font-size:12px;color:var(--muted);margin-left:auto;}
84
+ .tag{display:inline-flex;align-items:center;gap:3px;padding:4px 10px;border-radius:999px;font-size:12px;margin:2px;transition:var(--transition);cursor:default;}
85
+ .tag.clickable{cursor:pointer;user-select:none;}
86
+ .tag.clickable:hover{filter:brightness(.9);transform:scale(1.05);}
87
+ .tag.clickable:active{transform:scale(.97);}
88
+ .tag.copied{background:var(--green)!important;color:#fff!important;}
89
+
90
+ /* ─── Typography ─── */
91
+ .title{font-size:14px;font-weight:600;line-height:1.35;margin:6px 0;}
92
+ .card-wide .title{font-size:16px;font-weight:700;}
93
+ .caption{font-size:13px;color:var(--muted);line-height:1.5;margin:6px 0 0;}
94
+
95
+ /* ─── Stats ─── */
96
+ .stats{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:8px;}
97
+ .stat-pill{display:inline-flex;align-items:center;gap:4px;font-size:12px;color:var(--fg);background:var(--tag);padding:5px 12px;border-radius:999px;transition:var(--transition);}
98
+ .stat-pill:hover{background:var(--border);}
99
+
100
+ /* ─── Buttons ─── */
101
+ .btn{display:inline-flex;align-items:center;gap:6px;margin-top:12px;padding:8px 18px;color:#fff;border-radius:999px;text-decoration:none;font-size:13px;font-weight:600;transition:var(--transition);border:none;cursor:pointer;}
102
+ .btn:hover{filter:brightness(1.1);transform:translateY(-1px);box-shadow:var(--shadow-sm);}
103
+ .btn:active{transform:translateY(0);filter:brightness(.95);}
104
+ .btn-sm{padding:5px 12px;font-size:11px;margin-top:8px;}
105
+
106
+ /* ─── Sections (expandable) ─── */
107
+ .section{margin-bottom:14px;border-radius:var(--radius-sm);overflow:hidden;}
108
+ .section-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--tag);border-radius:var(--radius-sm);cursor:pointer;transition:var(--transition);user-select:none;}
109
+ .section-header:hover{background:var(--border);}
110
+ .section-label{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;}
111
+ .section-chevron{font-size:12px;color:var(--muted);transition:transform .2s ease;}
112
+ .section-header.open .section-chevron{transform:rotate(180deg);}
113
+ .section-content{padding:10px 12px;max-height:0;overflow:hidden;transition:max-height .3s ease,padding .3s ease;}
114
+ .section-content.open{max-height:600px;padding:10px 12px;}
115
+ .section-text{font-size:13px;color:var(--fg);line-height:1.6;}
116
+
117
+ /* ─── Progress bar ─── */
118
+ .progress-bar{height:6px;background:var(--tag);border-radius:999px;overflow:hidden;margin:6px 0;}
119
+ .progress-fill{height:100%;border-radius:999px;transition:width .6s cubic-bezier(.4,0,.2,1);background:linear-gradient(90deg,var(--blue),var(--green));}
120
+
121
+ /* ─── Hook strength ─── */
122
+ .hook-bar-track{display:flex;align-items:center;gap:10px;margin-top:6px;}
123
+ .hook-bar{flex:1;height:10px;background:var(--tag);border-radius:999px;overflow:hidden;}
124
+ .hook-bar-fill{height:100%;border-radius:999px;transition:width .8s cubic-bezier(.4,0,.2,1);}
125
+ .hook-score{font-size:15px;font-weight:800;min-width:40px;text-align:right;}
126
+
127
+ /* ─── Quote box ─── */
128
+ .quote-box{background:var(--tag);padding:10px 14px;border-radius:var(--radius-sm);font-size:13px;font-weight:600;line-height:1.45;margin:6px 0;border-left:3px solid var(--blue);position:relative;}
129
+ .quote-box .copy-btn{position:absolute;top:8px;right:8px;background:var(--card);border:1px solid var(--border);border-radius:6px;padding:3px 8px;font-size:10px;color:var(--muted);cursor:pointer;transition:var(--transition);opacity:0;}
130
+ .quote-box:hover .copy-btn{opacity:1;}
131
+ .copy-btn:hover{background:var(--accent);color:#fff;}
132
+
133
+ /* ─── Comments ─── */
134
+ .comment-row{padding:10px 0;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:flex-start;gap:10px;transition:var(--transition);}
135
+ .comment-row:last-child{border-bottom:none;}
136
+ .comment-row:hover{background:var(--tag);margin:0 -6px;padding:10px 6px;border-radius:var(--radius-sm);}
137
+
138
+ /* ─── Creator cards ─── */
139
+ .creator-card{border:1px solid var(--border);border-radius:var(--radius);padding:16px;width:230px;background:var(--card);display:inline-block;vertical-align:top;margin:4px;box-shadow:var(--shadow-sm);transition:var(--transition);}
140
+ .creator-card:hover{box-shadow:var(--shadow-md);transform:translateY(-2px);}
141
+ .avatar{width:52px;height:52px;border-radius:50%;object-fit:cover;border:2px solid var(--border);}
142
+ .avatar-placeholder{width:52px;height:52px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:22px;}
143
+ .verified{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;color:#fff;font-size:10px;margin-left:4px;}
144
+
145
+ /* ─── Sound cards ─── */
146
+ .sound-card{border:1px solid var(--border);border-radius:var(--radius);padding:14px;width:270px;background:var(--card);display:inline-block;vertical-align:top;margin:4px;box-shadow:var(--shadow-sm);transition:var(--transition);}
147
+ .sound-card:hover{box-shadow:var(--shadow-md);transform:translateY(-2px);}
148
+ .sound-cover{width:68px;height:68px;border-radius:var(--radius-sm);object-fit:cover;}
149
+
150
+ /* ─── Credits ─── */
151
+ .credits-card{border:1px solid var(--border);border-radius:16px;max-width:380px;background:var(--card);margin:8px 0;padding:24px;box-shadow:var(--shadow-md);}
152
+ .tier-badge{display:inline-flex;align-items:center;padding:4px 12px;background:linear-gradient(135deg,#eff6ff,#dbeafe);border-radius:999px;font-size:12px;color:#1e40af;font-weight:700;}
153
+ .free-tag{display:inline-flex;align-items:center;gap:3px;padding:4px 10px;background:linear-gradient(135deg,#d1fae5,#a7f3d0);border-radius:999px;font-size:11px;color:#065f46;font-weight:600;margin:2px;transition:var(--transition);}
154
+ .free-tag:hover{transform:scale(1.05);}
155
+
156
+ /* ─── JSON fallback ─── */
157
+ .json-block{background:var(--tag);border:1px solid var(--border);border-radius:var(--radius-sm);padding:14px;font-size:12px;font-family:'SF Mono',Monaco,monospace;white-space:pre-wrap;word-break:break-all;max-height:320px;overflow-y:auto;margin-top:10px;}
158
+
159
+ /* ─── Empty state ─── */
160
+ .empty-state{text-align:center;padding:48px 24px;color:var(--muted);}
161
+ .empty-state .icon{font-size:48px;margin-bottom:12px;}
162
+ .empty-state .text{font-size:14px;}
163
+ </style>
164
+ </head>
165
+ <body>
166
+ <div id="app">
167
+ <div class="loading-container">
168
+ <div class="loading-spinner"></div>
169
+ <div class="loading-text">Loading results…</div>
170
+ </div>
171
+ </div>
172
+ <script>
173
+ (function(){
174
+ var nextId=1;var pending=new Map();
175
+ var loadingTimer=null;
176
+
177
+ function send(method,params){
178
+ var id=nextId++;
179
+ return new Promise(function(resolve,reject){
180
+ pending.set(id,{resolve:resolve,reject:reject});
181
+ window.parent.postMessage({jsonrpc:"2.0",id:id,method:method,params:params},"*");
182
+ });
183
+ }
184
+
185
+ window.addEventListener("message",function(ev){
186
+ var d=ev.data;if(!d||typeof d!=="object")return;
187
+ if(d.id&&pending.has(d.id)){
188
+ var p=pending.get(d.id);pending.delete(d.id);
189
+ if(d.error)p.reject(new Error(d.error.message||JSON.stringify(d.error)));
190
+ else p.resolve(d.result);return;
191
+ }
192
+ if(d.method==="ui/notifications/tool-result"){
193
+ clearTimeout(loadingTimer);
194
+ render(d.params);
195
+ }
196
+ if(d.method==="ui/notifications/tool-input-partial"){
197
+ updateProgress(d.params);
198
+ }
199
+ });
200
+
201
+ // Show skeleton after 300ms of waiting (feels instant but shows loading for slow tools)
202
+ loadingTimer=setTimeout(function(){
203
+ var app=document.getElementById("app");
204
+ if(app)app.innerHTML=renderSkeleton();
205
+ },300);
206
+
207
+ // MCP Apps handshake
208
+ send("ui/initialize",{
209
+ protocolVersion:"2026-01-26",
210
+ capabilities:{},
211
+ clientInfo:{name:"orchyn-view",version:"2.0.0"}
212
+ }).then(function(){
213
+ window.parent.postMessage({jsonrpc:"2.0",method:"ui/notifications/initialized",params:{}},"*");
214
+ }).catch(function(){});
215
+
216
+ // ─── Helpers ───
217
+ function esc(s){return String(s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}
218
+ function fmtNum(n){n=Number(n)||0;return n>=1e6?(n/1e6).toFixed(1)+"M":n>=1e3?(n/1e3).toFixed(1)+"K":String(n);}
219
+ function pColor(p){return{tiktok:"#000",instagram:"#E1306C",youtube:"#FF0000",douyin:"#000",xiaohongshu:"#FF2442",twitter:"#1DA1F2",x:"#1DA1F2",bilibili:"#00A1D6"}[p]||"#6B7280";}
220
+ function pEmoji(p){return{tiktok:"🎵",instagram:"📸",youtube:"▶️",douyin:"🎶",xiaohongshu:"📕",twitter:"🐦",x:"🐦",bilibili:"📺"}[p]||"🔗";}
221
+
222
+ function copyText(text){
223
+ navigator.clipboard.writeText(text).catch(function(){});
224
+ }
225
+
226
+ // ─── Skeleton ───
227
+ function renderSkeleton(){
228
+ return '<div class="skeleton-gallery stagger">'
229
+ +Array(3).fill(0).map(function(){
230
+ return '<div class="skeleton-card">'
231
+ +'<div class="skeleton skeleton-thumb"></div>'
232
+ +'<div class="skeleton skeleton-badge"></div>'
233
+ +'<div class="skeleton skeleton-line medium"></div>'
234
+ +'<div class="skeleton skeleton-line short"></div>'
235
+ +'</div>';
236
+ }).join("")+'</div>';
237
+ }
238
+
239
+ // ─── Post Card ───
240
+ function postCard(p,wide){
241
+ var platform=p.platform||"unknown",color=pColor(platform),emoji=pEmoji(platform);
242
+ var title=p.title||p.caption||"",handle=p.creatorHandle||"",url=p.externalUrl||"";
243
+ var thumb=p.thumbnailUrl||"",ct=p.contentType||"post";
244
+ var views=p.views||0,likes=p.likes||0,comments=p.comments||0,shares=p.shares||0;
245
+ var cls=wide?"card card-wide":"card";
246
+ var statsHtml=[[views,"👁️"],[likes,"❤️"],[comments,"💬"],[shares,"🔄"]]
247
+ .filter(function(s){return s[0]>0})
248
+ .map(function(s){return'<span class="stat-pill">'+s[1]+" "+fmtNum(s[0])+"</span>";}).join("");
249
+ var handleHtml=handle?'<span class="handle">@'+esc(handle)+"</span>":"";
250
+ var linkHtml=url?'<a href="'+esc(url)+'" target="_blank" class="btn" style="background:'+color+'">View on '+esc(platform)+' →</a>':"";
251
+ var thumbHtml=thumb?'<img src="'+esc(thumb)+'" alt="thumbnail" loading="lazy"/>':"";
252
+ var ctTag=wide?'<span style="font-size:12px;color:var(--muted);text-transform:capitalize">'+esc(ct)+"</span>":"";
253
+ return '<div class="'+cls+'">'+thumbHtml+'<div class="card-body">'
254
+ +'<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px">'
255
+ +'<span class="badge" style="background:'+color+'15;color:'+color+'">'+emoji+" "+esc(platform)+"</span>"
256
+ +ctTag+handleHtml+"</div>"
257
+ +'<div class="title">'+esc(title.length>160?title.slice(0,160)+"…":title)+"</div>"
258
+ +'<div class="stats">'+statsHtml+"</div>"
259
+ +linkHtml+"</div></div>";
260
+ }
261
+
262
+ // ─── Creator Card ───
263
+ function creatorCard(c){
264
+ var platform=c.platform||"tiktok",color=pColor(platform),emoji=pEmoji(platform);
265
+ var username=c.username||c.uniqueId||"",nickname=c.nickname||c.displayName||username;
266
+ var followers=c.followers||c.followerCount||0,sig=(c.signature||c.bio||"").slice(0,100);
267
+ var verified=c.verified||false,avatar=c.avatarUrl||c.avatar_thumb||"";
268
+ var avatarHtml=avatar
269
+ ?'<img class="avatar" src="'+esc(avatar)+'" alt="avatar" loading="lazy"/>'
270
+ :'<div class="avatar-placeholder" style="background:'+color+'20">'+emoji+"</div>";
271
+ var vBadge=verified?'<span class="verified" style="background:'+color+'">✓</span>':"";
272
+ return '<div class="creator-card">'
273
+ +'<div style="display:flex;align-items:center;gap:12px;margin-bottom:10px">'
274
+ +avatarHtml+"<div>"
275
+ +'<div style="display:flex;align-items:center"><span style="font-size:14px;font-weight:700">'+esc(nickname)+"</span>"+vBadge+"</div>"
276
+ +'<div class="handle">@'+esc(username)+"</div></div></div>"
277
+ +'<div style="font-size:13px;color:var(--fg);font-weight:600;margin-bottom:6px">'+fmtNum(followers)+" followers</div>"
278
+ +sig?'<div style="font-size:12px;color:var(--muted);line-height:1.4">'+esc(sig)+"</div>":""
279
+ +"</div>";
280
+ }
281
+
282
+ // ─── Expandable Section ───
283
+ function section(label,contentHtml,openByDefault){
284
+ var id="sec_"+Math.random().toString(36).slice(2,8);
285
+ return '<div class="section">'
286
+ +'<div class="section-header'+(openByDefault?" open":"")+'" onclick="toggleSection(this)">'
287
+ +'<span class="section-label">'+esc(label)+'</span>'
288
+ +'<span class="section-chevron">▼</span></div>'
289
+ +'<div class="section-content'+(openByDefault?" open":"")+'">'+contentHtml+'</div></div>';
290
+ }
291
+
292
+ // ─── Analysis Card ───
293
+ function analysisCard(d){
294
+ var a=d.analysis||{},postHtml=postCard(d.post||d,true);var sections="";
295
+ if(a.summary)sections+=section("Summary",'<div class="section-text">'+esc(a.summary)+"</div>",true);
296
+ if(a.hookStrength!=null){
297
+ var hs=Number(a.hookStrength),pct=Math.min(hs/10*100,100);
298
+ var bc=hs>=7?"var(--green)":hs>=4?"var(--amber)":"var(--red)";
299
+ sections+='<div class="section" style="margin-bottom:14px"><div style="padding:10px 12px">'
300
+ +'<div class="section-label" style="margin-bottom:8px">Hook Strength</div>'
301
+ +'<div class="hook-bar-track">'
302
+ +'<div class="hook-bar"><div class="hook-bar-fill" style="width:'+pct+"%;background:"+bc+'"></div></div>'
303
+ +'<span class="hook-score" style="color:'+bc+'">'+hs+"/10</span></div></div></div>";
304
+ }
305
+ if(a.niche||a.visualStyle){
306
+ var tags="";
307
+ if(a.niche)tags+='<span class="tag clickable" onclick="copyText(this.dataset.v)" data-v="'+esc(a.niche)+'">📍 '+esc(a.niche)+"</span>";
308
+ if(a.visualStyle)tags+='<span class="tag">🎨 '+esc((a.visualStyle||"").slice(0,60))+"</span>";
309
+ sections+='<div style="margin-bottom:12px;display:flex;flex-wrap:wrap;gap:4px">'+tags+"</div>";
310
+ }
311
+ if(a.whyItWorks)sections+=section("Why It Works",'<div class="section-text">'+esc((a.whyItWorks||"").slice(0,400))+"</div>");
312
+ if(a.emotionalArc)sections+=section("Emotional Arc",'<div class="section-text">'+esc(a.emotionalArc)+"</div>");
313
+ if(a.viralTriggers&&a.viralTriggers.length){
314
+ var vt=a.viralTriggers.slice(0,8).map(function(t){return'<span class="tag clickable" onclick="copyText(this.dataset.v)" data-v="'+esc(t)+'">🔥 '+esc(t)+"</span>";}).join("");
315
+ sections+=section("Viral Triggers",vt);
316
+ }
317
+ if(a.suggestedHook)sections+=section("Suggested Hook",'<div class="quote-box">'+esc(a.suggestedHook)+'<button class="copy-btn" onclick="copyText(\\''+esc(a.suggestedHook).replace(/'/g,"\\\\'")+'\\')">Copy</button></div>',true);
318
+ if(a.suggestedHashtags&&a.suggestedHashtags.length){
319
+ var sh=a.suggestedHashtags.slice(0,10).map(function(t){var clean=t.replace(/^#/,"");return'<span class="tag clickable" onclick="copyText(this.dataset.v)" data-v="#'+esc(clean)+'" style="background:#ede9fe;color:#6d28d9">#'+esc(clean)+"</span>";}).join("");
320
+ sections+=section("Hashtags",sh);
321
+ }
322
+ if(a.formatBreakdown)sections+=section("Format Breakdown",'<div class="section-text">'+esc((a.formatBreakdown||"").slice(0,300))+"</div>");
323
+ if(a.negativeSignals&&a.negativeSignals.length){
324
+ var ns=a.negativeSignals.slice(0,6).map(function(n){return'<span class="tag" style="background:#fef2f2;color:#991b1b">⚠️ '+esc(n)+"</span>";}).join("");
325
+ sections+=section("Weaknesses",ns);
326
+ }
327
+ if(a.variationIdeas&&a.variationIdeas.length){
328
+ var vi=a.variationIdeas.slice(0,6).map(function(v){return'<li style="font-size:13px;color:var(--fg);margin:3px 0;padding-left:4px">'+esc(v)+"</li>";}).join("");
329
+ sections+=section("Variation Ideas",'<ul style="margin:0;padding-left:18px">'+vi+"</ul>");
330
+ }
331
+ if(!sections)return postHtml;
332
+ return postHtml+'<div class="card card-wide fade-in" style="margin-top:10px"><div class="card-body">'
333
+ +'<div style="font-size:16px;font-weight:700;margin-bottom:14px;display:flex;align-items:center;gap:8px">📊 AI Analysis</div>'
334
+ +sections+"</div></div>";
335
+ }
336
+
337
+ // ─── Render ───
338
+ function render(result){
339
+ var app=document.getElementById("app");if(!app)return;
340
+ var d=result&&result.structuredContent?result.structuredContent:result;
341
+ if(!d||typeof d!=="object"){app.innerHTML='<div class="json-block fade-in">'+esc(JSON.stringify(d,null,2))+"</div>";return;}
342
+
343
+ // Analysis
344
+ if(d.analysis&&(d.post||d.url)){app.innerHTML=analysisCard(d);return;}
345
+ // Posts gallery
346
+ if(d.posts&&Array.isArray(d.posts)){
347
+ if(!d.posts.length){app.innerHTML='<div class="empty-state fade-in"><div class="icon">🔍</div><div class="text">No posts found</div></div>';return;}
348
+ app.innerHTML='<div class="gallery stagger">'+d.posts.map(function(p){return postCard(p,false);}).join("")+"</div>";return;}
349
+ // Creators
350
+ if(d.creators&&Array.isArray(d.creators)){
351
+ if(!d.creators.length){app.innerHTML='<div class="empty-state fade-in"><div class="icon">👤</div><div class="text">No creators found</div></div>';return;}
352
+ app.innerHTML='<div class="gallery stagger">'+d.creators.map(function(c){return creatorCard(c);}).join("")+"</div>";return;}
353
+ // Comments
354
+ if(d.comments&&Array.isArray(d.comments)){
355
+ var ch=d.comments.slice(0,15).map(function(c){
356
+ var user=c.username||c.user||"",text=(c.text||c.comment||"").slice(0,140),likes=c.likes||c.likeCount||0;
357
+ return '<div class="comment-row"><div style="flex:1"><span style="font-size:12px;font-weight:600">@'+esc(user)+"</span>"
358
+ +'<div style="font-size:13px;color:var(--fg);line-height:1.45;margin-top:3px">'+esc(text)+"</div></div>"
359
+ +(likes>0?'<span style="font-size:11px;color:var(--muted);white-space:nowrap">❤️ '+fmtNum(likes)+"</span>":"")+"</div>";
360
+ }).join("");
361
+ app.innerHTML='<div class="card card-wide fade-in"><div class="card-body">'
362
+ +'<div style="font-size:16px;font-weight:700;margin-bottom:12px">💬 Comments ('+d.comments.length+')</div>'+ch+"</div></div>";return;}
363
+ // Sounds
364
+ if(d.sounds&&Array.isArray(d.sounds)){
365
+ app.innerHTML='<div class="gallery stagger">'+d.sounds.map(function(s){
366
+ var color=pColor(s.platform||"tiktok");
367
+ var cover=s.coverUrl?'<img class="sound-cover" src="'+esc(s.coverUrl)+'" alt="cover" loading="lazy"/>'
368
+ :'<div class="sound-cover" style="background:'+color+'15;display:flex;align-items:center;justify-content:center;font-size:26px">🎵</div>';
369
+ var play=s.playUrl||s.url||"";
370
+ var playBtn=play?'<a href="'+esc(play)+'" target="_blank" class="btn btn-sm" style="background:'+color+'">▶ Play</a>':"";
371
+ return '<div class="sound-card"><div style="display:flex;gap:12px;align-items:center">'
372
+ +cover+'<div style="flex:1;min-width:0">'
373
+ +'<div style="font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(s.title||"")+"</div>"
374
+ +'<div style="font-size:12px;color:var(--muted)">'+esc(s.artist||s.author||"")+(s.duration?" • "+esc(s.duration):"")+"</div>"
375
+ +playBtn+"</div></div></div>";
376
+ }).join("")+"</div>";return;}
377
+ // Credits
378
+ if(d.balance!=null||d.tier){
379
+ var bal=Number(d.balance)||0,tier=d.tier||"",ff=d.firstFreeTools||[];
380
+ var freeHtml="";
381
+ if(ff.length){var tags=ff.map(function(t){return'<span class="free-tag">✨ '+esc(t)+" free</span>";}).join("");
382
+ freeHtml='<div style="margin-top:12px"><div class="section-label" style="margin-bottom:6px">Free first uses remaining</div><div style="display:flex;flex-wrap:wrap;gap:4px">'+tags+"</div></div>";}
383
+ app.innerHTML='<div class="credits-card fade-in">'
384
+ +'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">'
385
+ +'<div style="font-size:16px;font-weight:700">💰 Your Credits</div>'
386
+ +(tier?'<span class="tier-badge">'+esc(tier)+"</span>":"")+"</div>"
387
+ +'<div style="font-size:36px;font-weight:800;margin:10px 0;letter-spacing:-1px">'+bal+"</div>"
388
+ +'<div style="font-size:13px;color:var(--muted)">credits remaining</div>'+freeHtml+"</div>";return;}
389
+ // Checkout
390
+ if(d.checkoutUrl||d.packs){
391
+ app.innerHTML='<div class="card card-wide fade-in"><div class="card-body">'
392
+ +'<div style="font-size:16px;font-weight:700;margin-bottom:14px">🛒 Credit Packs</div>'
393
+ +'<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px">'
394
+ +'<div style="border:1px solid var(--border);border-radius:10px;padding:16px;text-align:center;transition:var(--transition);cursor:pointer" onmouseover="this.style.borderColor=var(--muted)" onmouseout="this.style.borderColor=var(--border)"><div style="font-size:13px;font-weight:600">Starter</div><div style="font-size:24px;font-weight:800;margin:6px 0">$12.50</div><div style="font-size:12px;color:var(--muted)">500 credits</div></div>'
395
+ +'<div style="border:2px solid var(--accent);border-radius:10px;padding:16px;text-align:center;box-shadow:var(--shadow-md)"><div style="font-size:13px;font-weight:600">Pro ⭐</div><div style="font-size:24px;font-weight:800;margin:6px 0">$40</div><div style="font-size:12px;color:var(--muted)">2,000 credits</div></div>'
396
+ +'<div style="border:1px solid var(--border);border-radius:10px;padding:16px;text-align:center;transition:var(--transition);cursor:pointer" onmouseover="this.style.borderColor=var(--muted)" onmouseout="this.style.borderColor=var(--border)"><div style="font-size:13px;font-weight:600">Scale</div><div style="font-size:24px;font-weight:800;margin:6px 0">$85</div><div style="font-size:12px;color:var(--muted)">5,000 credits</div></div>'
397
+ +"</div></div></div>";return;}
398
+ // Single post
399
+ if(d.post||d.platform){app.innerHTML=postCard(d.post||d,true);return;}
400
+ // Fallback
401
+ app.innerHTML='<div class="json-block fade-in">'+esc(JSON.stringify(d,null,2))+"</div>";
402
+ }
403
+
404
+ function updateProgress(params){
405
+ var app=document.getElementById("app");
406
+ if(!app)return;
407
+ var text=params&&params.arguments?JSON.stringify(params.arguments):"Processing…";
408
+ app.innerHTML='<div class="loading-container fade-in">'
409
+ +'<div class="loading-spinner"></div>'
410
+ +'<div class="loading-text">'+esc(text.length>80?text.slice(0,80)+"…":text)+'</div>'
411
+ +'<div class="progress-bar" style="width:200px"><div class="progress-fill" style="width:60%"></div></div>'
412
+ +'</div>';
413
+ }
414
+
415
+ // Global helpers
416
+ window.copyText=copyText;
417
+ window.toggleSection=function(header){
418
+ header.classList.toggle("open");
419
+ var content=header.nextElementSibling;
420
+ if(content)content.classList.toggle("open");
421
+ };
422
+ })();
423
+ </script>
424
+ </body>
425
+ </html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchyn/mcp",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "description": "MCP server for orchyn - fetch, discover and understand TikTok/Instagram/YouTube/X posts with AI (media metadata, niche discovery, hook & viral analysis)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",