@nooticr/mcp 1.26.76 → 1.26.77

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.
@@ -36,7 +36,7 @@ import { registerAmazonTools } from "./amazon.js";
36
36
  import { registerMarketplaceTools } from "./marketplace.js";
37
37
  import { DEFAULT_NOOTICR_SETTINGS, DEFAULT_SOCIAL_PLATFORMS, MemorySettingsStore, SETTINGS_READ_TOOL, SETTINGS_UPDATE_TOOL, settingsOwnerFromAuth, } from "./settings.js";
38
38
  /** Current MCP server version — bumped on every deploy for traceability. */
39
- export const MCP_SERVER_VERSION = "1.26.76";
39
+ export const MCP_SERVER_VERSION = "1.26.77";
40
40
  /** MCP Apps extension identifier */
41
41
  const UI_EXTENSION = "io.modelcontextprotocol/ui";
42
42
  /** MIME type for MCP Apps HTML resources */
@@ -179,91 +179,21 @@ export function argumentsDigest(value) {
179
179
  return hash.toString(16);
180
180
  }
181
181
  /**
182
- * Fields that must reach the client exactly as the server wrote them.
182
+ * Why nothing here rewrites a URL (#242).
183
183
  *
184
- * The blanket rewrite below is deliberately indiscriminate about *images*,
185
- * but these are not images:
186
- * - `externalUrl` is the permalink on the social network. It is handed to
187
- * `ui/open-link` and opened in a real browser tab, so proxying it turns
188
- * "View on bilibili" into a fetch of the page through our image proxy.
189
- * - `embedUrl` is an <iframe> src on the platform's own player origin.
190
- * - the `*FallbackUrl` fields are already our own signed `/media/resolve`
191
- * capability links; wrapping one in `/media/proxy` proxies our own
192
- * resolver and invalidates nothing but the caller's playback.
193
- */
194
- const RAW_URL_KEYS = new Set([
195
- "externalUrl",
196
- "embedUrl",
197
- "videoFallbackUrl",
198
- "thumbnailFallbackUrl",
199
- "musicFallbackUrl",
200
- ]);
201
- /**
202
- * Whether a URL already points at one of our own media endpoints.
184
+ * This worker used to wrap every external URL in a result as
185
+ * `${server}/media/proxy?url=...`, so a card's images would load inside
186
+ * ChatGPT's and Claude's sandboxed frames. Those links were unsigned, and
187
+ * `/media/proxy` fetches from the internet for whoever calls it, so the server
188
+ * could not start refusing unsigned links without blanking every card.
203
189
  *
204
- * Checked by host-agnostic path, not by prefix: the server may mint these
205
- * against a different base than this process knows about, and a prefix test
206
- * silently misses that and double-wraps.
207
- */
208
- function isOwnMediaUrl(url) {
209
- // `/media/files` is nooticr's own re-hosted media — Weibo video arrives that
210
- // way. It was missing here, so whether it got wrapped a second time depended
211
- // on the host string matching NOOTICR_BASE_URL exactly: "localhost:8080" and
212
- // "127.0.0.1:8080" are the same server and different strings, and the proxy
213
- // then refused its own URL as an SSRF attempt. Recognising the path shape is
214
- // sturdier than trusting two spellings of a host to agree.
215
- return (url.includes("/media/proxy") ||
216
- url.includes("/media/resolve") ||
217
- url.includes("/media/files"));
218
- }
219
- /**
220
- * Rewrite external image URLs to go through the nooticr proxy so they
221
- * work inside ChatGPT's sandboxed iframe (CORS + CSP restrictions).
190
+ * nooticr-server#893 moved the rewrite to where the signing key is: the HTTP
191
+ * `tools/call` route now hands back every external URL as a signed proxy link
192
+ * (`&sig=`), with this worker's old rule ported word for word (permalinks,
193
+ * embeds and our own resolve/files/copy links stay as they are). So a result
194
+ * reaches the host exactly as the server wrote it. Minting a link here again,
195
+ * for any field, is the bug this note exists to prevent: it would be unsigned.
222
196
  */
223
- function proxyImageUrl(url) {
224
- if (!url || url.startsWith("data:"))
225
- return url;
226
- try {
227
- const u = new URL(url);
228
- // Only proxy external HTTP(S) URLs — skip our own proxy and data URIs
229
- if (u.protocol === "http:" || u.protocol === "https:") {
230
- const serverUrl = process.env.NOOTICR_API_URL || process.env.NOOTICR_BASE_URL || "";
231
- if (serverUrl && !url.startsWith(serverUrl) && !isOwnMediaUrl(url)) {
232
- return `${serverUrl.replace(/\/+$/, "")}/media/proxy?url=${encodeURIComponent(url)}`;
233
- }
234
- }
235
- }
236
- catch { }
237
- return url;
238
- }
239
- /** Recursively rewrite URL fields in a structured object for the proxy. */
240
- export function proxyUrls(obj) {
241
- if (typeof obj === "string") {
242
- // Check if it looks like a URL
243
- if (/^https?:\/\//.test(obj) && !isOwnMediaUrl(obj)) {
244
- return proxyImageUrl(obj);
245
- }
246
- return obj;
247
- }
248
- if (Array.isArray(obj))
249
- return obj.map(proxyUrls);
250
- if (obj && typeof obj === "object") {
251
- const out = {};
252
- for (const [k, v] of Object.entries(obj)) {
253
- if (RAW_URL_KEYS.has(k)) {
254
- out[k] = v;
255
- }
256
- else if (["thumbnailUrl", "preview_url", "coverUrl", "avatarUrl", "avatar_thumb", "image_url", "videoUrl", "video_url", "url"].includes(k) && typeof v === "string") {
257
- out[k] = proxyImageUrl(v);
258
- }
259
- else {
260
- out[k] = proxyUrls(v);
261
- }
262
- }
263
- return out;
264
- }
265
- return obj;
266
- }
267
197
  const structuredOf = (res) => (res.structured ?? {});
268
198
  /** A structured result this server computed rather than proxied. */
269
199
  function toStructured(value) {
@@ -312,9 +242,8 @@ async function toToolResult(proxy, guidance) {
312
242
  const htmlPrefix = rawText.startsWith("<")
313
243
  ? rawText.substring(0, rawText.indexOf("\n\n{")).trimEnd()
314
244
  : "";
315
- const structured = proxy.structured;
316
- // Proxy thumbnail URLs in structured content for ChatGPT iframe
317
- const proxied = structured ? proxyUrls(structured) : {};
245
+ // URLs arrive already signed by the server; see the note above structuredOf.
246
+ const structured = proxy.structured ?? {};
318
247
  // `_htmlCards` is the rendered card HTML, and the text block already carries
319
248
  // it as htmlPrefix below. Leaving it in the structured payload sent the same
320
249
  // HTML a second time to the model and a third time into the widget: measured
@@ -322,23 +251,15 @@ async function toToolResult(proxy, guidance) {
322
251
  // template reads (zero references in either embedded copy - views render from
323
252
  // `posts`). Dropped from both the JSON the model sees and the structured
324
253
  // content the widget receives; the prefix keeps the one copy that is used.
325
- const { _htmlCards: _renderedSeparately, ...forHosts } = proxied;
254
+ const { _htmlCards: _renderedSeparately, ...forHosts } = structured;
326
255
  const textJson = JSON.stringify(forHosts, null, 2);
327
- // Replace image URLs in HTML with proxied versions
328
- const proxiedHtml = htmlPrefix ? proxyImageUrlsInHtml(htmlPrefix) : "";
329
- const body = proxiedHtml ? `${proxiedHtml}\n\n${textJson}` : textJson;
256
+ const body = htmlPrefix ? `${htmlPrefix}\n\n${textJson}` : textJson;
330
257
  const text = guidance ? `${guidance}\n\n${body}` : body;
331
258
  return {
332
259
  content: [{ type: "text", text }],
333
260
  structuredContent: guidance ? { ...forHosts, guidance } : forHosts,
334
261
  };
335
262
  }
336
- /** Replace external image/video src, poster and href URLs in HTML with proxied versions. */
337
- function proxyImageUrlsInHtml(html) {
338
- return html.replace(/(src|href|poster)="(https?:\/\/[^"']+?)"/g, (_match, attr, url) => {
339
- return `${attr}="${proxyImageUrl(url)}"`;
340
- });
341
- }
342
263
  /**
343
264
  * An expired session, however the API happens to phrase it.
344
265
  *
@@ -4213,8 +4213,11 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
4213
4213
  +(coverSrc?'<img class="sound-cover" data-avatar src="'+esc(coverSrc)+'" alt="" loading="lazy"/>':"")+"</span>";
4214
4214
  // The raw CDN URL is signed and cross-origin — only the proxied one
4215
4215
  // actually plays in this sandboxed frame. A bare link never played
4216
- // anything in-place, which is why sound cards were silent.
4217
- var play=s.playUrl||s.playProxyUrl||s.url||"";
4216
+ // anything in-place, which is why sound cards were silent. So the
4217
+ // server's signed playProxyUrl comes first: playUrl is not a media key
4218
+ // the server proxies (nooticr-server#912), and this <audio> has no
4219
+ // retry to fall back on (#256).
4220
+ var play=s.playProxyUrl||s.playUrl||s.url||"";
4218
4221
  var secs=Number(s.duration)||0;
4219
4222
  var meta=[s.artist||s.author||"",secs>0?fmtTime(secs):""].filter(Boolean).join(" · ");
4220
4223
  var player=play
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nooticr/mcp",
3
3
  "mcpName": "com.nooticr/mcp",
4
- "version": "1.26.76",
4
+ "version": "1.26.77",
5
5
  "description": "MCP server for nooticr - fetch, discover and understand TikTok/Instagram/YouTube/X/Reddit/LinkedIn/Weibo posts with AI (media metadata, niche discovery, brand monitoring, hook & viral analysis)",
6
6
  "homepage": "https://mcp.nooticr.com",
7
7
  "repository": {