# Directory Structure ``` ├── .gitignore ├── chatbot │ ├── .gitignore │ ├── data │ │ └── install.sql │ ├── package.json │ ├── pnpm-lock.yaml │ ├── README_CN.md │ ├── README.md │ └── src │ ├── db.ts │ ├── index.ts │ ├── message.ts │ ├── types.d.ts │ └── utils.ts ├── package.json ├── pnpm-lock.yaml ├── preview.png ├── README_CN.md ├── README.md ├── src │ ├── index.ts │ ├── models │ │ ├── chat_message.ts │ │ └── db.ts │ ├── services │ │ ├── chat_message.ts │ │ └── tools.ts │ └── types │ └── chat_message.d.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules/ 2 | build/ 3 | *.log 4 | .env* 5 | data/* 6 | .vscode/* ``` -------------------------------------------------------------------------------- /chatbot/.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules/ 2 | .DS_Store 3 | data/chat.db 4 | .vscode/* 5 | .env* ``` -------------------------------------------------------------------------------- /chatbot/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # chatbot 2 | 3 | Save your chat messages to local sqlite database. 4 | 5 | [中文说明](README_CN.md) 6 | 7 | ## Prerequisites 8 | 9 | 1. Install `sqlite3` in your local machine. 10 | 11 | for macos: 12 | 13 | ```shell 14 | brew install sqlite3 15 | ``` 16 | 17 | 2. Set your environment variables. 18 | 19 | create `.env` file in the root directory, and set your chat database path. 20 | 21 | ```txt 22 | CHAT_DB_PATH=path-to/data/chat.db 23 | ``` 24 | 25 | 3. Init chat database. 26 | 27 | connect to your chat database with `sqlite3` command 28 | 29 | ```shell 30 | sqlite3 path-to/data/chat.db 31 | ``` 32 | 33 | create table `chat_messages` with `install.sql`. 34 | 35 | ```sql 36 | CREATE TABLE chat_messages ( 37 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 38 | created_at INTEGER NOT NULL, 39 | msg_id TEXT NOT NULL, 40 | room_id TEXT, 41 | room_name TEXT, 42 | room_avatar TEXT, 43 | talker_id TEXT NOT NULL, 44 | talker_name TEXT, 45 | talker_avatar TEXT, 46 | content TEXT, 47 | msg_type INTEGER, 48 | url_title TEXT, 49 | url_desc TEXT, 50 | url_link TEXT, 51 | url_thumb TEXT 52 | ); 53 | ``` 54 | 55 | ## Run chatbot 56 | 57 | 1. Install dependencies. 58 | 59 | ```shell 60 | pnpm install 61 | ``` 62 | 63 | 2. Start chatbot. 64 | 65 | ```shell 66 | pnpm start 67 | ``` 68 | 69 | 3. Login with your WeChat 70 | 71 | scan the QR code with your WeChat app. Let chatbot auto receive and save chat messages. 72 | 73 | > **Attention:** 74 | > 75 | > - chatbot use `wechaty` with `wechaty-puppet-wechat4u` to run RPA. 76 | > - it may be blocked by WeChat. Be careful with your WeChat account. 77 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # mcp-server-chatsum 2 | 3 | This MCP Server is used to summarize your chat messages. 4 | 5 | [中文说明](README_CN.md) 6 | 7 |  8 | 9 | > **Before you start** 10 | > 11 | > move to [chatbot](./chatbot) directory, follow the [README](./chatbot/README.md) to setup the chat database. 12 | > 13 | > start chatbot to save your chat messages. 14 | 15 | ## Features 16 | 17 | ### Resources 18 | 19 | ### Tools 20 | 21 | - `query_chat_messages` - Query chat messages 22 | - Query chat messages with given parameters 23 | - Summarize chat messages based on the query prompt 24 | 25 | ### Prompts 26 | 27 | ## Development 28 | 29 | 1. Set up environment variables: 30 | 31 | create `.env` file in the root directory, and set your chat database path. 32 | 33 | ```txt 34 | CHAT_DB_PATH=path-to/chatbot/data/chat.db 35 | ``` 36 | 37 | 2. Install dependencies: 38 | 39 | ```bash 40 | pnpm install 41 | ``` 42 | 43 | Build the server: 44 | 45 | ```bash 46 | pnpm build 47 | ``` 48 | 49 | For development with auto-rebuild: 50 | 51 | ```bash 52 | pnpm watch 53 | ``` 54 | 55 | ## Installation 56 | 57 | To use with Claude Desktop, add the server config: 58 | 59 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 60 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json` 61 | 62 | ```json 63 | { 64 | "mcpServers": { 65 | "mcp-server-chatsum": { 66 | "command": "path-to/bin/node", 67 | "args": ["path-to/mcp-server-chatsum/build/index.js"], 68 | "env": { 69 | "CHAT_DB_PATH": "path-to/mcp-server-chatsum/chatbot/data/chat.db" 70 | } 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ### Debugging 77 | 78 | Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script: 79 | 80 | ```bash 81 | pnpm inspector 82 | ``` 83 | 84 | The Inspector will provide a URL to access debugging tools in your browser. 85 | 86 | ## Community 87 | 88 | - [MCP Server Telegram](https://t.me/+N0gv4O9SXio2YWU1) 89 | - [MCP Server Discord](https://discord.gg/RsYPRrnyqg) 90 | 91 | ## About the author 92 | 93 | - [idoubi](https://bento.me/idoubi) 94 | ``` -------------------------------------------------------------------------------- /chatbot/src/types.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface ChatMessage { 2 | created_at: number; 3 | msg_id: string; 4 | room_id?: string; 5 | room_name?: string; 6 | room_avatar?: string; 7 | talker_id: string; 8 | talker_name?: string; 9 | talker_avatar?: string; 10 | content?: string; 11 | msg_type: number; 12 | url_title?: string; 13 | url_desc?: string; 14 | url_link?: string; 15 | url_thumb?: string; 16 | } 17 | ``` -------------------------------------------------------------------------------- /src/types/chat_message.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface ChatMessage { 2 | created_at: number; 3 | msg_id: string; 4 | room_id?: string; 5 | room_name?: string; 6 | room_avatar?: string; 7 | talker_id: string; 8 | talker_name?: string; 9 | talker_avatar?: string; 10 | content?: string; 11 | msg_type: number; 12 | url_title?: string; 13 | url_desc?: string; 14 | url_link?: string; 15 | url_thumb?: string; 16 | } 17 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | ``` -------------------------------------------------------------------------------- /src/models/db.ts: -------------------------------------------------------------------------------- ```typescript 1 | import sqlite3 from "sqlite3"; 2 | 3 | export function getDb(): sqlite3.Database { 4 | const dbName = process.env.CHAT_DB_PATH || ""; 5 | if (!dbName) { 6 | throw new Error("CHAT_DB_PATH is not set"); 7 | } 8 | 9 | const db = new sqlite3.Database(dbName, (err) => { 10 | if (err) { 11 | console.error("chat db connect failed: ", dbName, err.message); 12 | return; 13 | } 14 | }); 15 | 16 | return db; 17 | } 18 | ``` -------------------------------------------------------------------------------- /chatbot/data/install.sql: -------------------------------------------------------------------------------- ```sql 1 | CREATE TABLE chat_messages ( 2 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 3 | created_at INTEGER NOT NULL, 4 | msg_id TEXT NOT NULL, 5 | room_id TEXT, 6 | room_name TEXT, 7 | room_avatar TEXT, 8 | talker_id TEXT NOT NULL, 9 | talker_name TEXT, 10 | talker_avatar TEXT, 11 | content TEXT, 12 | msg_type INTEGER, 13 | url_title TEXT, 14 | url_desc TEXT, 15 | url_link TEXT, 16 | url_thumb TEXT 17 | ); ``` -------------------------------------------------------------------------------- /chatbot/src/message.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as PUPPET from "wechaty-puppet"; 2 | 3 | import { MessageInterface } from "wechaty/impls"; 4 | import { parseChatMessage } from "./utils"; 5 | import { saveChatMessage } from "./db"; 6 | 7 | export async function handleReceiveMessage(msg: MessageInterface) { 8 | try { 9 | console.log("receive message: ", msg); 10 | 11 | const m = await parseChatMessage(msg); 12 | 13 | if ( 14 | m.msg_type === PUPPET.types.Message.Text || 15 | m.msg_type === PUPPET.types.Message.Url 16 | ) { 17 | saveChatMessage(m); 18 | } 19 | } catch (e) { 20 | console.log("parse chat message failed: ", e); 21 | } 22 | } 23 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-server-chatsum", 3 | "version": "0.1.0", 4 | "description": "Summarize your chat messages.", 5 | "private": true, 6 | "type": "module", 7 | "bin": { 8 | "mcp-server-chatsum": "./build/index.js" 9 | }, 10 | "files": [ 11 | "build" 12 | ], 13 | "scripts": { 14 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 15 | "prepare": "npm run build", 16 | "watch": "tsc --watch", 17 | "inspector": "npx @modelcontextprotocol/inspector build/index.js" 18 | }, 19 | "dependencies": { 20 | "@modelcontextprotocol/sdk": "0.6.0", 21 | "dotenv": "^16.4.6", 22 | "sqlite3": "^5.1.7" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^20.17.9", 26 | "typescript": "^5.3.3" 27 | } 28 | } 29 | ``` -------------------------------------------------------------------------------- /src/services/tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const queryChatMessagesTool = { 2 | name: "query_chat_messages", 3 | description: "query chat messages with given parameters", 4 | inputSchema: { 5 | type: "object", 6 | properties: { 7 | room_names: { 8 | type: "array", 9 | description: "chat room names", 10 | items: { 11 | type: "string", 12 | description: "chat room name", 13 | }, 14 | }, 15 | talker_names: { 16 | type: "array", 17 | description: "talker names", 18 | items: { 19 | type: "string", 20 | description: "talker name", 21 | }, 22 | }, 23 | limit: { 24 | type: "number", 25 | description: "chat messages limit", 26 | default: 100, 27 | }, 28 | }, 29 | required: [], 30 | }, 31 | }; 32 | ``` -------------------------------------------------------------------------------- /chatbot/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-server-chatsum-bot", 3 | "version": "1.0.0", 4 | "description": "chatbot to save chat messages.", 5 | "main": "index.ts", 6 | "scripts": { 7 | "start": "ts-node src/index.ts" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mcpservers/mcp-server-chatsum.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "bugs": { 16 | "url": "https://github.com/mcpservers/mcp-server-chatsum/issues" 17 | }, 18 | "homepage": "https://github.com/mcpservers/mcp-server-chatsum#readme", 19 | "dependencies": { 20 | "dotenv": "^16.4.7", 21 | "qrcode-terminal": "^0.12.0", 22 | "sqlite3": "^5.1.6", 23 | "ts-node": "^10.9.2", 24 | "wechaty": "^1.20.2", 25 | "wechaty-puppet": "^1.20.2", 26 | "wechaty-puppet-padlocal": "^1.20.1" 27 | }, 28 | "devDependencies": { 29 | "@types/qrcode-terminal": "^0.12.0" 30 | } 31 | } 32 | ``` -------------------------------------------------------------------------------- /src/services/chat_message.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ChatMessage } from "../types/chat_message.js"; 2 | import { queryChatMessages } from "../models/chat_message.js"; 3 | 4 | export async function getChatMessages(params: any): Promise<ChatMessage[]> { 5 | const room_names: string[] = []; 6 | const talker_names: string[] = []; 7 | let page = 1; 8 | let limit = 100; 9 | 10 | if (params.limit && params.limit > 0 && params.limit < 1000) { 11 | limit = params.limit; 12 | } 13 | 14 | if (params.room_names) { 15 | if (Array.isArray(params.room_names)) { 16 | room_names.push(...params.room_names); 17 | } else { 18 | room_names.push(params.room_names); 19 | } 20 | } 21 | 22 | if (params.talker_names) { 23 | if (Array.isArray(params.talker_names)) { 24 | talker_names.push(...params.talker_names); 25 | } else { 26 | talker_names.push(params.talker_names); 27 | } 28 | } 29 | 30 | try { 31 | const result: ChatMessage[] = await queryChatMessages({ 32 | room_names, 33 | talker_names, 34 | page, 35 | limit, 36 | }); 37 | 38 | return result; 39 | } catch (error) { 40 | console.error("get chat messages failed: ", error); 41 | return []; 42 | } 43 | } 44 | ``` -------------------------------------------------------------------------------- /chatbot/src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ScanStatus, WechatyBuilder } from "wechaty"; 2 | 3 | import QrcodeTerminal from "qrcode-terminal"; 4 | import dotenv from "dotenv"; 5 | import { handleReceiveMessage } from "./message"; 6 | 7 | dotenv.config(); 8 | 9 | const token = ""; 10 | const bot = WechatyBuilder.build({ 11 | puppet: "wechaty-puppet-wechat4u", 12 | // puppet: 'wechaty-puppet-service', 13 | // puppet: "wechaty-puppet-padlocal", 14 | puppetOptions: { 15 | token, 16 | timeoutSeconds: 60, 17 | tls: { 18 | disable: true, 19 | // currently we are not using TLS since most puppet-service versions does not support it. See: https://github.com/wechaty/puppet-service/issues/160 20 | }, 21 | }, 22 | }); 23 | 24 | bot 25 | .on("scan", (qrcode, status, data) => { 26 | console.log(` 27 | ============================================================ 28 | qrcode : ${qrcode}, status: ${status}, data: ${data} 29 | ============================================================ 30 | `); 31 | if (status === ScanStatus.Waiting) { 32 | QrcodeTerminal.generate(qrcode, { 33 | small: true, 34 | }); 35 | } 36 | }) 37 | .on("login", (user) => { 38 | console.log(` 39 | ============================================ 40 | user: ${JSON.stringify(user)}, friend: ${user.friend()}, ${user.coworker()} 41 | ============================================ 42 | `); 43 | }) 44 | .on("message", handleReceiveMessage) 45 | .on("error", (err) => { 46 | console.log(err); 47 | }); 48 | 49 | bot.start(); 50 | ``` -------------------------------------------------------------------------------- /src/models/chat_message.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ChatMessage } from "../types/chat_message.js"; 2 | import { getDb } from "./db.js"; 3 | 4 | export async function queryChatMessages({ 5 | room_names, 6 | talker_names, 7 | page = 1, 8 | limit = 100, 9 | }: { 10 | room_names?: string[]; 11 | talker_names?: string[]; 12 | page?: number; 13 | limit?: number; 14 | }): Promise<ChatMessage[]> { 15 | try { 16 | const db = getDb(); 17 | if (!db) { 18 | throw new Error("db is not connected"); 19 | } 20 | 21 | const offset = (page - 1) * limit; 22 | 23 | let sql = `SELECT * FROM chat_messages`; 24 | let values: any[] = []; 25 | 26 | if (room_names && room_names.length > 0) { 27 | sql += ` WHERE room_name IN (${room_names.map(() => "?").join(",")})`; 28 | values.push(...room_names); 29 | } 30 | 31 | if (talker_names && talker_names.length > 0) { 32 | sql += ` WHERE talker_name IN (${talker_names.map(() => "?").join(",")})`; 33 | values.push(...talker_names); 34 | } 35 | 36 | sql += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`; 37 | values.push(limit, offset); 38 | 39 | console.error("query chat messages sql: ", sql, values); 40 | 41 | return new Promise((resolve, reject) => { 42 | db.all(sql, values, (err, rows) => { 43 | if (err) { 44 | console.error("query chat messages failed: ", err); 45 | reject(err); 46 | } else { 47 | resolve(rows as ChatMessage[]); 48 | } 49 | }); 50 | }); 51 | } catch (error) { 52 | console.error("query chat messages failed: ", error); 53 | throw error; 54 | } 55 | } 56 | ``` -------------------------------------------------------------------------------- /chatbot/src/db.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ChatMessage } from "./types"; 2 | import sqlite3 from "sqlite3"; 3 | 4 | export async function saveChatMessage(msg: ChatMessage) { 5 | try { 6 | const db = getDb(); 7 | if (!db) { 8 | throw new Error("db is not connected"); 9 | } 10 | 11 | const { 12 | msg_type, 13 | msg_id, 14 | created_at, 15 | room_id, 16 | room_name, 17 | room_avatar, 18 | talker_id, 19 | talker_name, 20 | talker_avatar, 21 | content, 22 | url_title, 23 | url_desc, 24 | url_link, 25 | url_thumb, 26 | } = msg; 27 | 28 | let sql = `INSERT INTO chat_messages( 29 | created_at, 30 | msg_id, 31 | room_id, 32 | room_name, 33 | room_avatar, 34 | talker_id, 35 | talker_name, 36 | talker_avatar, 37 | content, 38 | msg_type, 39 | url_title, 40 | url_desc, 41 | url_link, 42 | url_thumb 43 | ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`; 44 | 45 | db.run(sql, [ 46 | created_at, 47 | msg_id, 48 | room_id, 49 | room_name, 50 | room_avatar, 51 | talker_id, 52 | talker_name, 53 | talker_avatar, 54 | content, 55 | msg_type, 56 | url_title, 57 | url_desc, 58 | url_link, 59 | url_thumb, 60 | ]); 61 | 62 | console.error("save message ok"); 63 | } catch (error) { 64 | console.error("save message failed: ", error); 65 | throw error; 66 | } 67 | } 68 | 69 | export function getDb(): sqlite3.Database { 70 | const dbName = process.env.CHAT_DB_PATH || ""; 71 | if (!dbName) { 72 | throw new Error("CHAT_DB_PATH is not set"); 73 | } 74 | 75 | const db = new sqlite3.Database(dbName, (err) => { 76 | if (err) { 77 | console.error("chat db connect failed: ", dbName, err.message); 78 | return; 79 | } 80 | }); 81 | 82 | return db; 83 | } 84 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { 4 | CallToolRequestSchema, 5 | ListToolsRequestSchema, 6 | } from "@modelcontextprotocol/sdk/types.js"; 7 | 8 | import { ChatMessage } from "./types/chat_message.js"; 9 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 10 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 11 | import dotenv from "dotenv"; 12 | import { getChatMessages } from "./services/chat_message.js"; 13 | import { queryChatMessagesTool } from "./services/tools.js"; 14 | 15 | const server = new Server( 16 | { 17 | name: "mcp-server-chatsum", 18 | version: "0.1.0", 19 | }, 20 | { 21 | capabilities: { 22 | resources: {}, 23 | tools: {}, 24 | prompts: {}, 25 | }, 26 | } 27 | ); 28 | 29 | server.setRequestHandler(ListToolsRequestSchema, async () => { 30 | return { 31 | tools: [queryChatMessagesTool], 32 | }; 33 | }); 34 | 35 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 36 | console.error("call tool request:", request); 37 | switch (request.params.name) { 38 | case "query_chat_messages": { 39 | const messages: ChatMessage[] = await getChatMessages( 40 | request.params.arguments 41 | ); 42 | console.error("query chat messages result:", messages); 43 | 44 | return { 45 | content: [ 46 | { 47 | type: "text", 48 | text: JSON.stringify(messages), 49 | }, 50 | ], 51 | }; 52 | } 53 | 54 | default: 55 | throw new Error("Unknown tool"); 56 | } 57 | }); 58 | 59 | async function main() { 60 | const transport = new StdioServerTransport(); 61 | await server.connect(transport); 62 | } 63 | 64 | dotenv.config(); 65 | 66 | main().catch((error) => { 67 | console.error("Server error:", error); 68 | process.exit(1); 69 | }); 70 | ``` -------------------------------------------------------------------------------- /chatbot/src/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as PUPPET from "wechaty-puppet"; 2 | 3 | import { ChatMessage } from "./types"; 4 | import { MessageInterface } from "wechaty/impls"; 5 | 6 | export async function parseChatMessage( 7 | msg: MessageInterface 8 | ): Promise<ChatMessage> { 9 | const msg_type = msg.type(); 10 | const msg_id = msg.id; 11 | const payload = msg.payload; 12 | const talker = msg.talker(); 13 | 14 | if (!msg_id || !payload || !talker) { 15 | console.log("invalid msg: ", msg); 16 | return Promise.reject("invalid msg"); 17 | } 18 | 19 | let room_id = ""; 20 | let room_name = ""; 21 | let room_avatar = ""; 22 | 23 | const room = msg.room(); 24 | 25 | if (room) { 26 | room_id = room.id; 27 | room_name = (await room.topic()).trim(); 28 | room_avatar = room.payload?.avatar || ""; 29 | } 30 | 31 | const talker_id = talker.id; 32 | const talker_name = talker.name().trim(); 33 | const talker_avatar = talker.payload?.avatar; 34 | const created_at = payload.timestamp; 35 | 36 | let content = ""; 37 | 38 | let url_title = ""; 39 | let url_desc = ""; 40 | let url_link = ""; 41 | let url_thumb = ""; 42 | 43 | switch (msg_type) { 44 | case PUPPET.types.Message.Text: 45 | content = msg.text().trim(); 46 | break; 47 | case PUPPET.types.Message.Url: 48 | const urlMsg = await msg.toUrlLink(); 49 | 50 | url_title = urlMsg.title(); 51 | url_desc = urlMsg.description() || ""; 52 | url_link = urlMsg.url(); 53 | url_thumb = urlMsg.thumbnailUrl() || ""; 54 | break; 55 | default: 56 | console.log("msg type not support"); 57 | return Promise.reject(`msg type not support: ${msg_type}`); 58 | } 59 | 60 | return Promise.resolve({ 61 | msg_type, 62 | msg_id, 63 | created_at, 64 | talker_id, 65 | talker_name, 66 | talker_avatar, 67 | room_id, 68 | room_name, 69 | room_avatar, 70 | content, 71 | url_title, 72 | url_desc, 73 | url_link, 74 | url_thumb, 75 | }); 76 | } 77 | ```