@orchyn/mcp 1.7.4 → 1.9.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.
@@ -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")
@@ -17,7 +24,17 @@ function toToolResult(proxy) {
17
24
  data: String(c.data ?? ""),
18
25
  mimeType: String(c.mimeType ?? "image/jpeg"),
19
26
  }));
20
- const text = JSON.stringify(proxy.structured ?? {}, null, 2);
27
+ // The Rust backend embeds HTML cards directly in the text block
28
+ // (type:"text", text:"<div>...</div>\n\n{json}"). We extract the HTML
29
+ // prefix from the first text contentBlock and prepend it.
30
+ const textBlock = proxy.contentBlocks.find((c) => c.type === "text");
31
+ const rawText = textBlock ? String(textBlock.text ?? "") : "";
32
+ const htmlPrefix = rawText.startsWith("<")
33
+ ? rawText.substring(0, rawText.indexOf("\n\n{")).trimEnd()
34
+ : "";
35
+ const structured = proxy.structured;
36
+ const textJson = JSON.stringify(structured ?? {}, null, 2);
37
+ const text = htmlPrefix ? `${htmlPrefix}\n\n${textJson}` : textJson;
21
38
  return { content: [...images, { type: "text", text }] };
22
39
  }
23
40
  function toolError(prefix, err) {
@@ -25,15 +42,39 @@ function toolError(prefix, err) {
25
42
  return { content: [{ type: "text", text: `${prefix}: ${msg}` }], isError: true };
26
43
  }
27
44
  export function createMcpServer(makeClient) {
28
- const server = new McpServer({
29
- name: "orchyn-mcp",
30
- 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
+ },
31
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
+ }));
32
72
  server.registerTool("analyze_post", {
33
73
  title: "Analyze Post",
34
74
  description: "Analyze a social post (video, image, carousel/slideshow) from its link — " +
35
75
  "imports the media and runs AI analysis over the actual content (video frames, carousel images, caption). " +
36
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 } },
37
78
  inputSchema: z
38
79
  .object({
39
80
  url: z.string().describe("Public post URL (TikTok/Instagram/YouTube/X, Douyin, Xiaohongshu or Bilibili)."),
@@ -77,6 +118,7 @@ export function createMcpServer(makeClient) {
77
118
  description: "Fetch a social post's media from a TikTok, Instagram, YouTube, X/Twitter, Douyin, Xiaohongshu or Bilibili URL: " +
78
119
  "contentType (video/image/carousel/slideshow), title, caption, author, stats and direct media URLs. " +
79
120
  "Returns an inline thumbnail image. Consumes 1 orchyn credit. First use free per user.",
121
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
80
122
  inputSchema: z
81
123
  .object({
82
124
  url: z.string().describe("Full public post URL."),
@@ -96,6 +138,7 @@ export function createMcpServer(makeClient) {
96
138
  description: "Discover recent posts (video, image, carousel, slideshow) for a niche on YouTube, TikTok, Instagram, Douyin, Xiaohongshu, X/Twitter or Bilibili. " +
97
139
  "Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
98
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 } },
99
142
  inputSchema: z
100
143
  .object({
101
144
  niche: z.string().describe("Niche/topic, e.g. 'fitness'."),
@@ -122,6 +165,7 @@ export function createMcpServer(makeClient) {
122
165
  description: "List recent posts by a creator handle (e.g. @zoundsapp) on TikTok, Instagram, YouTube, Douyin, Xiaohongshu, X/Twitter or Bilibili. " +
123
166
  "Each post includes title/caption, thumbnailUrl, externalUrl, views/likes/comments and inline thumbnails (up to 4) so they show in chat. " +
124
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 } },
125
169
  inputSchema: z
126
170
  .object({
127
171
  username: z.string().describe("Creator handle, e.g. 'zoundsapp' or '@zoundsapp'."),
@@ -146,6 +190,7 @@ export function createMcpServer(makeClient) {
146
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, " +
147
191
  "then synthesize a profile report — creator summary, niche, content themes, hook styles, strengths/weaknesses, " +
148
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 } },
149
194
  inputSchema: z
150
195
  .object({
151
196
  username: z.string().describe("Creator handle, e.g. 'zoundsapp'."),
@@ -170,6 +215,7 @@ export function createMcpServer(makeClient) {
170
215
  title: "Get Post Comments",
171
216
  description: "Fetch top comments for a post URL on TikTok, Instagram, YouTube, Douyin, X/Twitter or Bilibili, plus keyword clusters from TikTok Analytics " +
172
217
  "when available — audience sentiment/audience-signal analysis. Consumes 2 orchyn credits. First use free per user.",
218
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
173
219
  inputSchema: z
174
220
  .object({
175
221
  url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/Douyin/X/Bilibili)."),
@@ -189,6 +235,7 @@ export function createMcpServer(makeClient) {
189
235
  title: "Search Creators",
190
236
  description: "Search creators by niche/keyword on TikTok, Instagram, Xiaohongshu, YouTube or Douyin — username, nickname, follower count, " +
191
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 } },
192
239
  inputSchema: z
193
240
  .object({
194
241
  keyword: z.string().describe("Niche/keyword, e.g. 'fitness' or a creator name."),
@@ -212,6 +259,7 @@ export function createMcpServer(makeClient) {
212
259
  title: "Get Similar Creators",
213
260
  description: "Find lookalike creators for a given handle — TikTok similar-user recommendations or Instagram " +
214
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 } },
215
263
  inputSchema: z
216
264
  .object({
217
265
  username: z.string().describe("Seed creator handle, e.g. 'zoundsapp'."),
@@ -231,6 +279,7 @@ export function createMcpServer(makeClient) {
231
279
  title: "Discover Sounds",
232
280
  description: "Discover trending sounds/music for a keyword on TikTok or Instagram — the sound is a huge ranking " +
233
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 } },
234
283
  inputSchema: z
235
284
  .object({
236
285
  keyword: z.string().describe("Niche/keyword, e.g. 'gym'."),
@@ -250,6 +299,7 @@ export function createMcpServer(makeClient) {
250
299
  server.registerTool("check_orchyn_credits", {
251
300
  title: "Check Orchyn Credits",
252
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 } },
253
303
  inputSchema: z.object({}).strict(),
254
304
  }, async (_args, extra) => {
255
305
  const client = await makeClient(extra);
@@ -263,6 +313,7 @@ export function createMcpServer(makeClient) {
263
313
  server.registerTool("buy_orchyn_credits", {
264
314
  title: "Buy Orchyn Credits",
265
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 } },
266
317
  inputSchema: z.object({}).strict(),
267
318
  }, async (_args, extra) => {
268
319
  const client = await makeClient(extra);
@@ -278,6 +329,7 @@ export function createMcpServer(makeClient) {
278
329
  description: "Import a social post URL AND understand it with multimodal AI over the actual video/images: " +
279
330
  "summary, hook strength, viral triggers, format breakdown and variation ideas. Includes the thumbnail. " +
280
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 } },
281
333
  inputSchema: z
282
334
  .object({
283
335
  url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/X/Douyin/Xiaohongshu/Bilibili)."),
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Single unified HTML template for all Orchyn MCP tools.
3
+ *
4
+ * The host renders this in a sandboxed iframe. After the MCP Apps handshake,
5
+ * the host pushes the tool result via `ui/notifications/tool-result`.
6
+ * The template auto-detects the data shape and renders the appropriate view.
7
+ *
8
+ * No external dependencies — pure HTML + CSS + JS, fully inline.
9
+ */
10
+ export const ORCHYN_UI_TEMPLATE = `<!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="UTF-8"/>
14
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
15
+ <title>Orchyn</title>
16
+ <style>
17
+ :root {
18
+ --bg: #ffffff; --fg: #111827; --muted: #6b7280; --border: #e5e7eb;
19
+ --card-bg: #ffffff; --tag-bg: #f3f4f6; --accent: #14151a;
20
+ --tiktok: #000; --instagram: #E1306C; --youtube: #FF0000;
21
+ --douyin: #000; --xiaohongshu: #FF2442; --twitter: #1DA1F2; --bilibili: #00A1D6;
22
+ }
23
+ @media(prefers-color-scheme:dark){
24
+ :root {
25
+ --bg: #1a1a2e; --fg: #f0f0f0; --muted: #9ca3af; --border: #374151;
26
+ --card-bg: #16213e; --tag-bg: #1f2937; --accent: #e2e8f0;
27
+ }
28
+ }
29
+ * { margin:0; padding:0; box-sizing:border-box; }
30
+ body { font-family: system-ui,-apple-system,sans-serif; background:var(--bg); color:var(--fg); padding:12px; }
31
+ .loading { color:var(--muted); font-size:14px; text-align:center; padding:40px 0; }
32
+ .gallery { display:flex; flex-wrap:wrap; gap:10px; }
33
+ .card {
34
+ border:1px solid var(--border); border-radius:12px; overflow:hidden;
35
+ width:300px; background:var(--card-bg); display:inline-block;
36
+ vertical-align:top; box-shadow:0 1px 3px rgba(0,0,0,0.06);
37
+ transition: box-shadow 0.2s;
38
+ }
39
+ .card:hover { box-shadow:0 4px 12px rgba(0,0,0,0.12); }
40
+ .card-wide { max-width:520px; width:100%; }
41
+ .card img { width:100%; height:200px; object-fit:cover; display:block; }
42
+ .card-wide img { max-height:340px; }
43
+ .card-body { padding:12px 14px; }
44
+ .badge {
45
+ display:inline-block; padding:2px 8px; border-radius:999px;
46
+ font-size:11px; font-weight:700; text-transform:capitalize;
47
+ }
48
+ .handle { font-size:12px; color:var(--muted); }
49
+ .title { font-size:14px; font-weight:600; line-height:1.3; margin:6px 0; }
50
+ .card-wide .title { font-size:16px; font-weight:700; }
51
+ .stats { display:flex; flex-wrap:wrap; gap:4px; align-items:center; }
52
+ .stat {
53
+ display:inline-flex; align-items:center; gap:3px;
54
+ font-size:12px; color:var(--muted);
55
+ }
56
+ .stat-pill {
57
+ font-size:13px; color:var(--fg); background:var(--tag-bg);
58
+ padding:4px 10px; border-radius:999px;
59
+ }
60
+ .dot { color:var(--border); margin:0 2px; }
61
+ .btn {
62
+ display:inline-block; margin-top:10px; padding:6px 14px;
63
+ color:#fff; border-radius:999px; text-decoration:none;
64
+ font-size:12px; font-weight:600; transition:opacity 0.2s;
65
+ }
66
+ .btn:hover { opacity:0.85; }
67
+ .section { margin-bottom:12px; }
68
+ .section-label {
69
+ font-size:11px; font-weight:700; color:var(--muted);
70
+ text-transform:uppercase; letter-spacing:0.05em; margin-bottom:4px;
71
+ }
72
+ .section-text { font-size:13px; color:var(--fg); line-height:1.5; }
73
+ .tag {
74
+ display:inline-block; padding:3px 10px; border-radius:999px;
75
+ font-size:12px; margin:2px;
76
+ }
77
+ .bar-track {
78
+ height:8px; background:var(--tag-bg); border-radius:999px;
79
+ overflow:hidden; flex:1;
80
+ }
81
+ .bar-fill { height:100%; border-radius:999px; transition:width 0.4s ease; }
82
+ .quote-box {
83
+ background:var(--tag-bg); padding:8px 12px; border-radius:8px;
84
+ font-size:13px; font-weight:600; line-height:1.4; margin:4px 0;
85
+ }
86
+ .comment-row {
87
+ padding:8px 0; border-bottom:1px solid var(--border);
88
+ display:flex; justify-content:space-between; align-items:flex-start; gap:8px;
89
+ }
90
+ .comment-row:last-child { border-bottom:none; }
91
+ .creator-card {
92
+ border:1px solid var(--border); border-radius:12px; padding:14px;
93
+ width:220px; background:var(--card-bg); display:inline-block;
94
+ vertical-align:top; margin:4px; box-shadow:0 1px 3px rgba(0,0,0,0.05);
95
+ }
96
+ .avatar {
97
+ width:48px; height:48px; border-radius:50%; object-fit:cover;
98
+ }
99
+ .avatar-placeholder {
100
+ width:48px; height:48px; border-radius:50%;
101
+ display:flex; align-items:center; justify-content:center; font-size:20px;
102
+ }
103
+ .verified {
104
+ display:inline-block; width:16px; height:16px; border-radius:50%;
105
+ color:#fff; font-size:10px; line-height:16px; text-align:center; margin-left:4px;
106
+ }
107
+ .sound-card {
108
+ border:1px solid var(--border); border-radius:12px; padding:12px;
109
+ width:260px; background:var(--card-bg); display:inline-block;
110
+ vertical-align:top; margin:4px; box-shadow:0 1px 3px rgba(0,0,0,0.05);
111
+ }
112
+ .sound-cover {
113
+ width:64px; height:64px; border-radius:8px; object-fit:cover;
114
+ }
115
+ .credits-card {
116
+ border:1px solid var(--border); border-radius:14px; max-width:360px;
117
+ background:var(--card-bg); margin:8px 0; padding:20px;
118
+ box-shadow:0 2px 8px rgba(0,0,0,0.06);
119
+ }
120
+ .tier-badge {
121
+ display:inline-block; padding:3px 10px; background:#eff6ff;
122
+ border-radius:999px; font-size:11px; color:#1e40af; font-weight:600;
123
+ }
124
+ .free-tag {
125
+ display:inline-block; padding:3px 8px; background:#d1fae5;
126
+ border-radius:999px; font-size:11px; color:#065f46; margin:2px;
127
+ }
128
+ .json-block {
129
+ background:var(--tag-bg); border:1px solid var(--border);
130
+ border-radius:8px; padding:12px; font-size:12px;
131
+ font-family:monospace; white-space:pre-wrap; word-break:break-all;
132
+ max-height:300px; overflow-y:auto; margin-top:8px;
133
+ }
134
+ </style>
135
+ </head>
136
+ <body>
137
+ <div id="app"><div class="loading">Loading…</div></div>
138
+ <script>
139
+ (function(){
140
+ let nextId=1;
141
+ const pending=new Map();
142
+
143
+ function send(method,params){
144
+ const id=nextId++;
145
+ return new Promise((resolve,reject)=>{
146
+ pending.set(id,{resolve,reject});
147
+ window.parent.postMessage({jsonrpc:"2.0",id,method,params},"*");
148
+ });
149
+ }
150
+
151
+ window.addEventListener("message",function(ev){
152
+ const d=ev.data;
153
+ if(!d||typeof d!=="object")return;
154
+ if(d.id&&pending.has(d.id)){
155
+ const p=pending.get(d.id);
156
+ pending.delete(d.id);
157
+ if(d.error)p.reject(new Error(d.error.message||JSON.stringify(d.error)));
158
+ else p.resolve(d.result);
159
+ return;
160
+ }
161
+ if(d.method==="ui/notifications/tool-result"){
162
+ render(d.params);
163
+ }
164
+ if(d.method==="ui/notifications/tool-input"){
165
+ // Tool input available — could show loading state
166
+ }
167
+ });
168
+
169
+ // MCP Apps handshake
170
+ send("ui/initialize",{
171
+ protocolVersion:"2026-01-26",
172
+ capabilities:{},
173
+ clientInfo:{name:"orchyn-view",version:"1.0.0"}
174
+ }).then(function(){
175
+ window.parent.postMessage({jsonrpc:"2.0",method:"ui/notifications/initialized",params:{}},"*");
176
+ }).catch(function(){});
177
+
178
+ function esc(s){return String(s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}
179
+ function fmtNum(n){n=Number(n)||0;return n>=1e6?(n/1e6).toFixed(1)+"M":n>=1e3?(n/1e3).toFixed(1)+"K":String(n);}
180
+ function pColor(p){return{tiktok:"#000",instagram:"#E1306C",youtube:"#FF0000",douyin:"#000",xiaohongshu:"#FF2442",twitter:"#1DA1F2",x:"#1DA1F2",bilibili:"#00A1D6"}[p]||"#6B7280";}
181
+ function pEmoji(p){return{tiktok:"🎵",instagram:"📸",youtube:"▶️",douyin:"🎶",xiaohongshu:"📕",twitter:"🐦",x:"🐦",bilibili:"📺"}[p]||"🔗";}
182
+
183
+ function postCard(p,wide){
184
+ var platform=p.platform||"unknown";
185
+ var color=pColor(platform);
186
+ var emoji=pEmoji(platform);
187
+ var title=p.title||p.caption||"";
188
+ var handle=p.creatorHandle||"";
189
+ var url=p.externalUrl||"";
190
+ var thumb=p.thumbnailUrl||"";
191
+ var ct=p.contentType||"post";
192
+ var views=p.views||0,likes=p.likes||0,comments=p.comments||0,shares=p.shares||0;
193
+ var cls=wide?"card card-wide":"card";
194
+ var statsHtml="";
195
+ [[views,"👁️"],[likes,"❤️"],[comments,"💬"],[shares,"🔄"]].forEach(function(s){
196
+ if(s[0]>0)statsHtml+='<span class="stat">'+s[1]+" "+fmtNum(s[0])+"</span>";
197
+ });
198
+ var handleHtml=handle?'<span class="handle">@'+esc(handle)+"</span>":"";
199
+ var linkHtml=url?'<a href="'+esc(url)+'" target="_blank" class="btn" style="background:'+color+'">View on '+esc(platform)+" →</a>":"";
200
+ var thumbHtml=thumb?'<img src="'+esc(thumb)+'" alt="thumbnail"/>':"";
201
+ var ctTag=wide?'<span style="font-size:12px;color:var(--muted);text-transform:capitalize">'+esc(ct)+"</span>":"";
202
+ return '<div class="'+cls+'">'+thumbHtml+'<div class="card-body">'
203
+ +'<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">'
204
+ +'<span class="badge" style="background:'+color+'15;color:'+color+'">'+emoji+" "+esc(platform)+"</span>"
205
+ +ctTag+handleHtml+"</div>"
206
+ +'<div class="title">'+esc(title.length>160?title.slice(0,160)+"…":title)+"</div>"
207
+ +'<div class="stats">'+statsHtml+"</div>"
208
+ +linkHtml+"</div></div>";
209
+ }
210
+
211
+ function creatorCard(c){
212
+ var platform=c.platform||"tiktok";
213
+ var color=pColor(platform);
214
+ var emoji=pEmoji(platform);
215
+ var username=c.username||c.uniqueId||"";
216
+ var nickname=c.nickname||c.displayName||username;
217
+ var followers=c.followers||c.followerCount||0;
218
+ var sig=(c.signature||c.bio||"").slice(0,100);
219
+ var verified=c.verified||false;
220
+ var avatar=c.avatarUrl||c.avatar_thumb||"";
221
+ var avatarHtml=avatar
222
+ ?'<img class="avatar" src="'+esc(avatar)+'" alt="avatar"/>'
223
+ :'<div class="avatar-placeholder" style="background:'+color+'20">'+emoji+"</div>";
224
+ var vBadge=verified?'<span class="verified" style="background:'+color+'">✓</span>':"";
225
+ return '<div class="creator-card">'
226
+ +'<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">'
227
+ +avatarHtml+"<div>"
228
+ +'<div style="display:flex;align-items:center"><span style="font-size:14px;font-weight:700">'+esc(nickname)+"</span>"+vBadge+"</div>"
229
+ +'<div class="handle">@'+esc(username)+"</div></div></div>"
230
+ +'<div style="font-size:12px;color:var(--fg);font-weight:600;margin-bottom:4px">'+fmtNum(followers)+" followers</div>"
231
+ +sig?'<div style="font-size:12px;color:var(--muted);line-height:1.4">'+esc(sig)+"</div>":""
232
+ +"</div>";
233
+ }
234
+
235
+ function analysisCard(d){
236
+ var a=d.analysis||{};
237
+ var postHtml=postCard(d.post||d,true);
238
+ var sections="";
239
+ if(a.summary)sections+='<div class="section"><div class="section-label">Summary</div><div class="section-text">'+esc(a.summary)+"</div></div>";
240
+ if(a.hookStrength!=null){
241
+ var hs=Number(a.hookStrength);
242
+ var pct=Math.min(hs/10*100,100);
243
+ var bc=hs>=7?"#10b981":hs>=4?"#f59e0b":"#ef4444";
244
+ sections+='<div class="section"><div class="section-label">Hook Strength</div>'
245
+ +'<div style="display:flex;align-items:center;gap:8px"><div class="bar-track"><div class="bar-fill" style="width:'+pct+"%;background:"+bc+'"></div></div>'
246
+ +'<span style="font-size:13px;font-weight:700">'+hs+"/10</span></div></div>";
247
+ }
248
+ if(a.niche)sections+='<div class="section"><span class="tag" style="background:var(--tag-bg);color:var(--fg)">📍 '+esc(a.niche)+"</span></div>";
249
+ if(a.whyItWorks)sections+='<div class="section"><div class="section-label">Why It Works</div><div class="section-text">'+esc((a.whyItWorks||"").slice(0,300))+"</div></div>";
250
+ if(a.emotionalArc)sections+='<div class="section"><div class="section-label">Emotional Arc</div><div class="section-text">'+esc(a.emotionalArc)+"</div></div>";
251
+ if(a.viralTriggers&&a.viralTriggers.length){
252
+ var vt=a.viralTriggers.slice(0,6).map(function(t){return'<span class="tag" style="background:#fef3c7;color:#92400e">🔥 '+esc(t)+"</span>";}).join("");
253
+ sections+='<div class="section"><div class="section-label">Viral Triggers</div>'+vt+"</div>";
254
+ }
255
+ if(a.suggestedHook)sections+='<div class="section"><div class="section-label">Suggested Hook</div><div class="quote-box">💡 '+esc(a.suggestedHook)+"</div></div>";
256
+ if(a.suggestedHashtags&&a.suggestedHashtags.length){
257
+ var sh=a.suggestedHashtags.slice(0,8).map(function(t){return'<span class="tag" style="background:#ede9fe;color:#6d28d9">#'+esc(t.replace(/^#/,""))+"</span>";}).join("");
258
+ sections+='<div class="section"><div class="section-label">Hashtags</div>'+sh+"</div>";
259
+ }
260
+ if(a.formatBreakdown)sections+='<div class="section"><div class="section-label">Format</div><div class="section-text">'+esc((a.formatBreakdown||"").slice(0,250))+"</div></div>";
261
+ if(a.negativeSignals&&a.negativeSignals.length){
262
+ var ns=a.negativeSignals.slice(0,5).map(function(n){return'<span class="tag" style="background:#fef2f2;color:#991b1b">⚠️ '+esc(n)+"</span>";}).join("");
263
+ sections+='<div class="section"><div class="section-label">Weaknesses</div>'+ns+"</div>";
264
+ }
265
+ if(a.variationIdeas&&a.variationIdeas.length){
266
+ var vi=a.variationIdeas.slice(0,5).map(function(v){return"<li>"+esc(v)+"</li>";}).join("");
267
+ sections+='<div class="section"><div class="section-label">Variation Ideas</div><ul style="margin:0;padding-left:18px">'+vi+"</ul></div>";
268
+ }
269
+ if(!sections)return postHtml;
270
+ return postHtml+'<div class="card card-wide" style="margin-top:8px"><div class="card-body">'
271
+ +'<div style="font-size:15px;font-weight:700;margin-bottom:14px">📊 AI Analysis</div>'
272
+ +sections+"</div></div>";
273
+ }
274
+
275
+ function render(result){
276
+ var app=document.getElementById("app");
277
+ if(!app)return;
278
+ var d=result&&result.structuredContent?result.structuredContent:result;
279
+ if(!d||typeof d!=="object"){app.innerHTML='<div class="json-block">'+esc(JSON.stringify(d,null,2))+"</div>";return;}
280
+
281
+ // Detect shape
282
+ if(d.analysis&&(d.post||d.url))return void(app.innerHTML=analysisCard(d));
283
+ if(d.posts&&Array.isArray(d.posts)){
284
+ var gallery=d.posts.map(function(p){return postCard(p,false);}).join("");
285
+ return void(app.innerHTML='<div class="gallery">'+gallery+"</div>");
286
+ }
287
+ if(d.creators&&Array.isArray(d.creators)){
288
+ var cg=d.creators.map(function(c){return creatorCard(c);}).join("");
289
+ return void(app.innerHTML='<div class="gallery">'+cg+"</div>");
290
+ }
291
+ if(d.comments&&Array.isArray(d.comments)){
292
+ var ch=d.comments.slice(0,15).map(function(c){
293
+ var user=c.username||c.user||"";
294
+ var text=(c.text||c.comment||"").slice(0,120);
295
+ var likes=c.likes||c.likeCount||0;
296
+ return '<div class="comment-row"><div style="flex:1"><span style="font-size:12px;font-weight:600">@'+esc(user)+"</span>"
297
+ +'<div style="font-size:13px;color:var(--fg);line-height:1.4;margin-top:2px">'+esc(text)+"</div></div>"
298
+ +(likes>0?'<span style="font-size:11px;color:var(--muted);white-space:nowrap">❤️ '+fmtNum(likes)+"</span>":"")+"</div>";
299
+ }).join("");
300
+ return void(app.innerHTML='<div class="card card-wide"><div class="card-body">'
301
+ +'<div style="font-size:15px;font-weight:700;margin-bottom:10px">💬 Comments</div>'+ch+"</div></div>");
302
+ }
303
+ if(d.sounds&&Array.isArray(d.sounds)){
304
+ var snds=d.sounds.map(function(s){
305
+ var color=pColor(s.platform||"tiktok");
306
+ var cover=s.coverUrl?'<img class="sound-cover" src="'+esc(s.coverUrl)+'" alt="cover"/>'
307
+ :'<div class="sound-cover" style="background:'+color+'15;display:flex;align-items:center;justify-content:center;font-size:24px">🎵</div>';
308
+ var play=s.playUrl||s.url||"";
309
+ var playBtn=play?'<a href="'+esc(play)+'" target="_blank" class="btn" style="background:'+color+';margin-top:6px;padding:4px 12px;font-size:11px">▶ Play</a>':"";
310
+ return '<div class="sound-card"><div style="display:flex;gap:10px;align-items:center">'
311
+ +cover+'<div style="flex:1;min-width:0">'
312
+ +'<div style="font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(s.title||"")+"</div>"
313
+ +'<div style="font-size:12px;color:var(--muted)">'+esc(s.artist||s.author||"")+(s.duration?" • "+esc(s.duration):"")+"</div>"
314
+ +playBtn+"</div></div></div>";
315
+ }).join("");
316
+ return void(app.innerHTML='<div class="gallery">'+snds+"</div>");
317
+ }
318
+ if(d.balance!=null||d.tier){
319
+ var bal=Number(d.balance)||0;
320
+ var tier=d.tier||"";
321
+ var ff=d.firstFreeTools||[];
322
+ var freeHtml="";
323
+ if(ff.length){
324
+ var tags=ff.map(function(t){return'<span class="free-tag">✨ '+esc(t)+" free</span>";}).join("");
325
+ freeHtml='<div style="margin-top:10px"><div class="section-label">Free first uses remaining</div><div style="display:flex;flex-wrap:wrap;gap:4px">'+tags+"</div></div>";
326
+ }
327
+ return void(app.innerHTML='<div class="credits-card">'
328
+ +'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">'
329
+ +'<div style="font-size:15px;font-weight:700">💰 Your Credits</div>'
330
+ +(tier?'<span class="tier-badge">'+esc(tier)+"</span>":"")+"</div>"
331
+ +'<div style="font-size:32px;font-weight:800;margin:8px 0">'+bal+"</div>"
332
+ +'<div style="font-size:13px;color:var(--muted)">credits remaining</div>'
333
+ +freeHtml+"</div>");
334
+ }
335
+ if(d.checkoutUrl||d.packs){
336
+ return void(app.innerHTML='<div class="card card-wide"><div class="card-body">'
337
+ +'<div style="font-size:15px;font-weight:700;margin-bottom:12px">🛒 Credit Packs</div>'
338
+ +'<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px">'
339
+ +'<div style="border:1px solid var(--border);border-radius:10px;padding:14px;text-align:center"><div style="font-size:13px;font-weight:600">Starter</div><div style="font-size:22px;font-weight:800;margin:4px 0">$12.50</div><div style="font-size:12px;color:var(--muted)">500 credits</div></div>'
340
+ +'<div style="border:2px solid var(--accent);border-radius:10px;padding:14px;text-align:center;box-shadow:0 4px 12px rgba(0,0,0,0.08)"><div style="font-size:13px;font-weight:600">Pro</div><div style="font-size:22px;font-weight:800;margin:4px 0">$40</div><div style="font-size:12px;color:var(--muted)">2,000 credits</div></div>'
341
+ +'<div style="border:1px solid var(--border);border-radius:10px;padding:14px;text-align:center"><div style="font-size:13px;font-weight:600">Scale</div><div style="font-size:22px;font-weight:800;margin:4px 0">$85</div><div style="font-size:12px;color:var(--muted)">5,000 credits</div></div>'
342
+ +"</div></div></div>");
343
+ }
344
+ // Single post (get_social_media)
345
+ if(d.post||d.platform){
346
+ return void(app.innerHTML=postCard(d.post||d,true));
347
+ }
348
+ // Fallback: pretty-print JSON
349
+ app.innerHTML='<div class="json-block">'+esc(JSON.stringify(d,null,2))+"</div>";
350
+ }
351
+ })();
352
+ </script>
353
+ </body>
354
+ </html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchyn/mcp",
3
- "version": "1.7.4",
3
+ "version": "1.9.0",
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",