1(() => { 2 'use strict'; 3 4 const config = window.CoffeeLaboWebMCPConfig || {}; 5 const TOOL_NAMES = [ 6 'search_coffee_articles', 7 'get_coffee_article', 8 'browse_coffee_topics', 9 'answer_coffee_question', 10 ]; 11 const OUTPUT_BUDGET = 1450; 12 const state = { 13 initialized: false, 14 supported: false, 15 registered: [], 16 error: null, 17 }; 18 let initializationPromise = null; 19 20 const clampText = (value, max = OUTPUT_BUDGET) => { 21 const text = String(value ?? '').trim(); 22 return text.length <= max ? text : `${text.slice(0, Math.max(1, max - 1)).trimEnd()}â¦`; 23 }; 24 25 const normalizeInteger = (value, fallback, min, max) => { 26 const parsed = Number.parseInt(value, 10); 27 if (!Number.isFinite(parsed)) return fallback; 28 return Math.max(min, Math.min(max, parsed)); 29 }; 30 31 const pushMetric = (toolName, status, resultCount = 0) => { 32 if (!config.analytics) return; 33 const analytics = window.__clwmcpAnalyticsRuntime; 34 if (!analytics || typeof analytics.send !== 'function') return; 35 analytics.send('webmcp_tool_call', { 36 webmcp_tool_name: toolName, 37 webmcp_status: status, 38 webmcp_result_count: Number(resultCount) || 0, 39 }); 40 }; 41 42 const fetchWithTimeout = async (url, options = {}, timeoutMs = 38000) => { 43 const controller = new AbortController(); 44 const timer = window.setTimeout(() => controller.abort(), timeoutMs); 45 try { 46 return await fetch(url, { ...options, signal: controller.signal }); 47 } catch (error) { 48 if (error?.name === 'AbortError') { 49 throw new Error('Request timed out.'); 50 } 51 throw error; 52 } finally { 53 window.clearTimeout(timer); 54 } 55 }; 56 57 const apiRequest = async (path, params = {}) => { 58 const base = String(config.restBase || ''); 59 if (!base) throw new Error('WebMCP REST endpoint is not configured.'); 60 61 const url = new URL(path.replace(/^\//, ''), base); 62 Object.entries(params).forEach(([key, value]) => { 63 if (value !== undefined && value !== null && value !== '') { 64 url.searchParams.set(key, String(value)); 65 } 66 }); 67 68 const response = await fetchWithTimeout(url.toString(), { 69 method: 'GET', 70 credentials: 'same-origin', 71 headers: { Accept: 'application/json' }, 72 }, 15000); 73 74 let payload = null; 75 try { 76 payload = await response.json(); 77 } catch (_) { 78 // The status text below remains useful if a proxy returns non-JSON. 79 } 80 81 if (!response.ok) { 82 throw new Error(payload?.message || `Request failed (${response.status}).`); 83 } 84 return payload; 85 }; 86 87 88 const apiPost = async (urlValue, body = {}) => { 89 const url = String(urlValue || ''); 90 if (!url) throw new Error('AI answer endpoint is not configured.'); 91 92 const response = await fetchWithTimeout(url, { 93 method: 'POST', 94 credentials: 'same-origin', 95 headers: { 96 Accept: 'application/json', 97 'Content-Type': 'application/json', 98 }, 99 body: JSON.stringify(body), 100 }, 42000); 101 102 let payload = null; 103 try { 104 payload = await response.json(); 105 } catch (_) { 106 // The status text below remains useful if a proxy returns non-JSON. 107 } 108 109 if (!response.ok) { 110 throw new Error(payload?.message || `Request failed (${response.status}).`); 111 } 112 return payload; 113 }; 114 115 const formatSearch = (data) => { 116 const items = Array.isArray(data?.items) ? data.items : []; 117 if (!items.length) { 118 return `ã${data?.query || ''}ãã«ä¸è´ããå ¬éè¨äºã¯è¦ã¤ããã¾ããã§ãããå¥ã®èªå¥ã¾ãã¯browse_coffee_topicsã®ãã¼ãã§åæ¤ç´¢ãã¦ãã ããã`; 119 } 120 121 const lines = [`æ¤ç´¢çµæ: å ¨${data.total_found ?? items.length}ä»¶ä¸${items.length}ä»¶`]; 122 items.forEach((item, index) => { 123 const topics = Array.isArray(item.categories) 124 ? item.categories.map((term) => term.name).filter(Boolean).slice(0, 3).join('ã') 125 : ''; 126 lines.push( 127 `${index + 1}. [ID ${item.id}] ${item.title}`, 128 `URL: ${item.url}`, 129 topics ? `ãã¼ã: ${topics}` : '', 130 item.summary ? `è¦ç´: ${item.summary}` : '', 131 item.modified ? `æ´æ°: ${item.modified}` : '' 132 ); 133 });
134 return clampText(lines.filter(Boolean).join('\n')); 135 }; 136 137 const formatArticle = (data) => { 138 const topics = Array.isArray(data?.categories) 139 ? data.categories.map((term) => term.name).filter(Boolean).join('ã') 140 : ''; 141 return clampText([ 142 data?.title || 'è¨äº', 143 data?.url ? `URL: ${data.url}` : '', 144 data?.author ? `èè : ${data.author}` : '', 145 topics ? `ãã¼ã: ${topics}` : '', 146 data?.modified ? `æ´æ°: ${data.modified}` : '', 147 data?.content ? `æ¬ææç²: ${data.content}` : (data?.excerpt ? `è¦ç´: ${data.excerpt}` : ''), 148 data?.truncated ? '注: æ¬æã¯WebMCPã®åºåä¸éã«åããã¦æç²ãã¦ãã¾ãã' : '', 149 ].filter(Boolean).join('\n')); 150 }; 151 152 const formatTopics = (data) => { 153 const items = Array.isArray(data?.items) ? data.items : []; 154 if (!items.length) return 'ä¸è´ããè¨äºãã¼ãã¯è¦ã¤ããã¾ããã§ããã'; 155 return clampText([ 156 `è¨äºãã¼ã ${items.length}ä»¶:`, 157 ...items.map((item) => `- ${item.name}ï¼${item.count}è¨äº / slug: ${item.slug}ï¼ ${item.url}`), 158 ].join('\n')); 159 }; 160 161 162 const formatAnswer = (data) => { 163 const sources = Array.isArray(data?.sources) ? data.sources : []; 164 const lines = [data?.answer || 'åçãåå¾ã§ãã¾ããã§ããã']; 165 if (data?.response_mode === 'free_fallback') lines.push('注: AIåçã®ä»£ããã«è¨äºæ¬æã®é¢é£ç®æã表示ãã¦ãã¾ãã'); 166 if (data?.safety_note) lines.push(`注æ: ${data.safety_note}`); 167 if (sources.length) { 168 lines.push('åèè¨äº:'); 169 sources.forEach((source, index) => { 170 lines.push(`${index + 1}. ${source.title} ${source.url}`); 171 }); 172 } 173 if (data?.shop_cta?.url) { 174 lines.push(`${data.shop_cta.label || 'ã©ãã«ãã§ãè¦ã'}: ${data.shop_cta.url}`); 175 } 176 return clampText(lines.join('\n')); 177 }; 178 179 const tools = [ 180 { 181 name: 'search_coffee_articles', 182 description: 'ã³ã¼ãã¼è±ç ç©¶æã®å ¬éè¨äºããã¼ã¯ã¼ãã§æ¤ç´¢ãã¾ããã³ã¼ãã¼è±ãå¨å ·ãæ·¹ãæ¹ãã«ãã§ãå¥åº·ãã¹ã¿ã¼ããã¯ã¹ãªã©ã®è§£èª¬è¨äºãæ¢ãã¨ãã«ä½¿ãã¾ãã', 183 inputSchema: { 184 type: 'object', 185 additionalProperties: false, 186 properties: { 187 query: { 188 type: 'string', 189 minLength: 2, 190 maxLength: 100, 191 description: 'å©ç¨è ãç¥ãããå 容ãèªç¶ãªæ¥æ¬èªã§æå®ãã¾ãã', 192 }, 193 topic: { 194 type: 'string', 195 description: 'ä»»æãbrowse_coffee_topicsã§å¾ããã¼ãåã¾ãã¯slugã§ãã', 196 }, 197 sort: { 198 type: 'string', 199 enum: ['relevance', 'newest', 'updated'], 200 description: 'é¢é£åº¦ãæ°çãæ´æ°æ¥ã®ãããããæ¢å®ã¯é¢é£åº¦ã§ãã', 201 }, 202 limit: { 203 type: 'integer', 204 minimum: 1, 205 maximum: 5, 206 description: 'è¿ãè¨äºæ°ã1ã5ä»¶ã§ãã', 207 }, 208 }, 209 required: ['query'], 210 }, 211 execute: async ({ query, topic = '', sort = 'relevance', limit = config.maxResults || 5 }) => { 212 const toolName = 'search_coffee_articles'; 213 try { 214 const data = await apiRequest('search', { 215 q: String(query || '').trim(), 216 topic: String(topic || '').trim(), 217 sort, 218 limit: normalizeInteger(limit, config.maxResults || 5, 1, 5), 219 }); 220 pushMetric(toolName, 'success', data?.returned || 0); 221 return formatSearch(data); 222 } catch (error) { 223 pushMetric(toolName, 'error', 0); 224 throw new Error(`è¨äºæ¤ç´¢ã«å¤±æãã¾ãã: ${error.message}`); 225 } 226 }, 227 annotations: { readOnlyHint: true, untrustedContentHint: true }, 228 }, 229 { 230 name: 'get_coffee_article', 231 description: 'ã³ã¼ãã¼è±ç ç©¶æã®å ¬éè¨äº1ä»¶ãããã¿ã¤ãã«ãURLãèè ãæ´æ°æ¥ãã«ãã´ãªãæ¬ææç²ãåå¾ãã¾ããæ¤ç´¢çµæã詳ãã確èªããã¨ãã«ä½¿ãã¾ãã', 232 inputSchema: { 233 type: 'object', 234 additionalProperties: false, 235 properties: { 236 article: { 237 type: 'string', 238 maxLength: 500, 239 description: 'è¨äºIDãè¨äºslugãã¾ãã¯coffee-labo.co.jpã®è¨äºURLã§ãã', 240 }, 241 max_chars: { 242 type: 'integer', 243 minimum: 500, 244 maximum: 1200, 245 description: 'æ¬ææç²ã®æå¤§æåæ°ãæ¢å®ã¯ãµã¤ãè¨å®å¤ã§ãã', 246 }, 247 }, 248 required: ['article'], 249 }, 250 execute: async ({ article, max_chars = config.maxArticleChars || 1000 }) => { 251 const toolName = 'get_coffee_article'; 252 try { 253 const data = await apiRequest('article', { 254 article: String(article || '').trim(), 255 max_chars: normalizeInteger(max_chars, config.maxArticleChars || 1000, 500, 1200), 256 }); 257 pushMetric(toolName, 'success', 1); 258 return formatArticle(data); 259 } catch (error) { 260 pushMetric(toolName, 'error', 0); 261 throw new Error(`è¨äºåå¾ã«å¤±æãã¾ãã: ${error.message}`); 262 } 263 }, 264 annotations: { readOnlyHint: true, untrustedContentHint: true }, 265 }, 266 { 267 name: 'browse_coffee_topics', 268 description: 'ã³ã¼ãã¼è±ç ç©¶æã®è¨äºã«ãã´ãªããè¨äºæ°ãslugãã«ãã´ãªURLã¨ã¨ãã«è¡¨ç¤ºãã¾ããæ¤ç´¢å¯¾è±¡ã®ãã¼ããæ±ºããã¨ãã«ä½¿ãã¾ãã', 269 inputSchema: { 270 type: 'object', 271 additionalProperties: false, 272 properties: { 273 query: { 274 type: 'string', 275 maxLength: 50, 276 description: 'ä»»æãã«ãã´ãªåãçµãèªå¥ã§ããä¾: å¨å ·ãã¹ã¿ããæ·¹ãæ¹ã', 277 }, 278 limit: { 279 type: 'integer', 280 minimum: 1, 281 maximum: 12, 282 description: 'è¿ããã¼ãæ°ã1ã12ä»¶ã§ãã', 283 }, 284 }, 285 }, 286 execute: async ({ query = '', limit = 12 } = {}) => { 287 const toolName = 'browse_coffee_topics'; 288 try { 289 const data = await apiRequest('topics', { 290 q: String(query || '').trim(), 291 limit: normalizeInteger(limit, 12, 1, 12), 292 }); 293 pushMetric(toolName, 'success', data?.returned || 0); 294 return formatTopics(data); 295 } catch (error) { 296 pushMetric(toolName, 'error', 0); 297 throw new Error(`ãã¼ãåå¾ã«å¤±æãã¾ãã: ${error.message}`); 298 } 299 }, 300 annotations: { readOnlyHint: true, untrustedContentHint: false }, 301 }, 302 {
303 name: 'answer_coffee_question', 304 description: config.aiWebMcpMode === 'openai' 305 ? 'ã³ã¼ãã¼è±ç ç©¶æã®å ¬éè¨äºã ããæ ¹æ ã«ãã³ã¼ãã¼ã«é¢ãã質åã¸çãåçããåèè¨äºãæç¤ºãã¾ããç¾å¨ã®è¨äºãããå ´åã¯ãã®å 容ãåªå ãã¾ãã' 306 : 'ã³ã¼ãã¼è±ç ç©¶æã®å ¬éè¨äºãã質åã«é¢é£ããæ¬æç®æã¨åèè¨äºãæç¤ºãã¾ããå¤é¨ã®çæAIã¯ä½¿ç¨ãã¾ããã', 307 inputSchema: { 308 type: 'object', 309 additionalProperties: false, 310 properties: { 311 question: { 312 type: 'string', 313 minLength: 6, 314 maxLength: 300, 315 description: 'å©ç¨è ã®ã³ã¼ãã¼ã«é¢ãã質åã§ãã', 316 }, 317 }, 318 required: ['question'], 319 }, 320 execute: async ({ question }) => { 321 const toolName = 'answer_coffee_question'; 322 if (!config.aiEnabled) throw new Error('ãµã¤ãå ã®è³ªåã»æ¤ç´¢æ©è½ã¯ç¾å¨ç¡å¹ã§ãã'); 323 try { 324 const data = await apiPost(config.aiRestUrl, { 325 question: String(question || '').trim(), 326 post_id: Number(config.aiPostId || 0), 327 token: String(config.aiToken || ''), 328 channel: 'webmcp', 329 }); 330 pushMetric(toolName, 'success', Array.isArray(data?.sources) ? data.sources.length : 0); 331 return formatAnswer(data); 332 } catch (error) { 333 pushMetric(toolName, 'error', 0); 334 throw new Error(`è¨äºããã®åçåå¾ã«å¤±æãã¾ãã: ${error.message}`); 335 } 336 }, 337 annotations: { readOnlyHint: true, untrustedContentHint: true }, 338 }, 339 ]; 340 341 const activeTools = config.aiEnabled 342 ? tools 343 : tools.filter((tool) => tool.name !== 'answer_coffee_question'); 344 const activeToolNames = activeTools.map((tool) => tool.name); 345 346 const waitForModelContext = async () => { 347 for (let attempt = 0; attempt < 20; attempt += 1) { 348 if (document.modelContext?.registerTool) return document.modelContext; 349 await new Promise((resolve) => window.setTimeout(resolve, 250)); 350 } 351 return null; 352 }; 353 354 const initialize = () => { 355 if (initializationPromise) return initializationPromise; 356 357 initializationPromise = (async () => { 358 state.initialized = true; 359 try { 360 const modelContext = await waitForModelContext(); 361 if (!modelContext) { 362 state.supported = false; 363 return state; 364 } 365 366 state.supported = true; 367 for (const tool of activeTools) { 368 await modelContext.registerTool(tool); 369 state.registered.push(tool.name); 370 } 371 } catch (error) { 372 state.error = error instanceof Error ? error.message : String(error); 373 console.error('[Coffee Labo WebMCP]', error); 374 } 375 return state; 376 })(); 377 378 return initializationPromise; 379 }; 380 381 window.CoffeeLaboWebMCP = Object.freeze({ 382 version: config.version || '0.3.9', 383 toolNames: [...activeToolNames], 384 initialize, 385 getStatus: async () => { 386 await initialize(); 387 const available = document.modelContext?.getTools 388 ? (await document.modelContext.getTools()).filter((tool) => activeToolNames.includes(tool.name)).map((tool) => tool.name) 389 : []; 390 return { ...state, available }; 391 }, 392 execute: async (name, args = {}) => { 393 await initialize(); 394 if (!document.modelContext?.getTools || !document.modelContext?.executeTool) { 395 throw new Error('ãã®ãã©ã¦ã¶ã§ã¯WebMCPãå®è¡ã§ãã¾ããã'); 396 } 397 const available = await document.modelContext.getTools(); 398 const tool = available.find((candidate) => candidate.name === name && activeToolNames.includes(candidate.name)); 399 if (!tool) throw new Error(`WebMCP tool not found: ${name}`); 400 return document.modelContext.executeTool(tool, JSON.stringify(args)); 401 }, 402 }); 403 404 void initialize(); 405})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.