# Directory Structure
```
├── .env.example
├── .gitignore
├── jest.config.js
├── logo.png
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── config
│   │   └── env.ts
│   ├── index.ts
│   ├── services
│   │   └── tools.ts
│   ├── tools
│   │   ├── __tests__
│   │   │   └── search-stock-news.test.ts
│   │   ├── drama-search.ts
│   │   ├── general-search.ts
│   │   └── search-stock-news.ts
│   └── types
│       └── tools.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
 1 | # Tavily API Configuration
 2 | TAVILY_API_KEY=your_api_key_here
 3 | 
 4 | # Search Configuration
 5 | MAX_RESULTS=20
 6 | SEARCH_DEPTH=basic
 7 | MIN_SCORE=0.4
 8 | 
 9 | # Query Templates (comma-separated)
10 | QUERY_TEMPLATES={symbol} ({company_name}) latest stock price movements, {symbol} ({company_name}) earnings reports
11 | 
12 | # Domain Configuration (comma-separated)
13 | INCLUDE_DOMAINS=https://cafef.vn,https://nguoiquansat.vn
14 | EXCLUDE_DOMAINS=rs.nguoiquansat.vn,en.vneconomy.vn,en.vietstock.vn
15 | 
16 | # Server Configuration
17 | TRANSPORT_TYPE=stdio
18 | PORT=8080 
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
 1 | # Dependencies
 2 | node_modules/
 3 | npm-debug.log*
 4 | yarn-debug.log*
 5 | yarn-error.log*
 6 | .pnpm-debug.log*
 7 | 
 8 | # TypeScript
 9 | *.tsbuildinfo
10 | dist/
11 | build/
12 | out/
13 | 
14 | # IDE and editors
15 | .idea/
16 | .vscode/
17 | *.swp
18 | *.swo
19 | .DS_Store
20 | .env.local
21 | .env.development.local
22 | .env.test.local
23 | .env.production.local
24 | 
25 | # Testing
26 | coverage/
27 | 
28 | # Debug
29 | .debug/
30 | 
31 | # Environment variables
32 | .env
33 | .env.*
34 | !.env.example
35 | 
36 | # Logs
37 | logs/
38 | *.log
39 | 
40 | # Optional npm cache directory
41 | .npm
42 | 
43 | # Optional eslint cache
44 | .eslintcache
45 | 
46 | # Optional stylelint cache
47 | .stylelintcache
48 | 
49 | # Yarn
50 | .yarn/*
51 | !.yarn/patches
52 | !.yarn/plugins
53 | !.yarn/releases
54 | !.yarn/sdks
55 | !.yarn/versions
56 | .pnp.* 
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
  1 | <h1 align="center">Search Stock News MCP Server 🚀</h1>
  2 | 
  3 | <p align="center">
  4 |   <img src="logo.png" width="300" alt="VLD Logo">
  5 | </p>
  6 | 
  7 | > 🔌 **Compatible with Cline, Cursor, Claude Desktop, and any other MCP Clients!**
  8 | > 
  9 | > Search Stock News MCP works seamlessly with any MCP client
 10 | 
 11 | The Model Context Protocol (MCP) is an open standard that enables AI systems to interact seamlessly with various data sources and tools, facilitating secure, two-way connections.
 12 | 
 13 | The Search Stock News MCP server provides:
 14 | 
 15 | * Real-time stock news search capabilities via Tavily API
 16 | * Multiple customizable search query templates
 17 | * Configurable search parameters and filtering
 18 | * Domain-specific content filtering
 19 | * Type-safe operations with TypeScript
 20 | 
 21 | ## Prerequisites 🔧
 22 | 
 23 | Before you begin, ensure you have:
 24 | 
 25 | * Tavily API Key
 26 | * Claude Desktop, Cursor, or any MCP-compatible client
 27 | * Node.js (v16 or higher)
 28 | * Git installed (only needed if using Git installation method)
 29 | 
 30 | ## Search Stock News MCP Server Installation ⚡
 31 | 
 32 | ### Running with NPX
 33 | 
 34 | ```bash
 35 | npx -y search-stock-news-mcp@latest
 36 | ```
 37 | 
 38 | ### Installing via Smithery
 39 | 
 40 | To install Search Stock News MCP Server for Claude Desktop automatically via Smithery:
 41 | 
 42 | ```bash
 43 | npx -y @smithery/cli install search-stock-news-mcp --client claude
 44 | ```
 45 | 
 46 | ## Configuring MCP Clients ⚙️
 47 | 
 48 | ### Configuring Cline 🤖
 49 | 
 50 | The easiest way to set up the Search Stock News MCP server in Cline is through the marketplace:
 51 | 
 52 | 1. Open Cline in VS Code
 53 | 2. Click on the Cline icon in the sidebar
 54 | 3. Navigate to the "MCP Servers" tab
 55 | 4. Search "Search Stock News" and click "install"
 56 | 5. When prompted, enter your Tavily API key
 57 | 
 58 | Alternatively, manually configure the server in Cline:
 59 | 
 60 | 1. Open the Cline MCP settings file:
 61 | ```bash
 62 | # For macOS:
 63 | code ~/Library/Application\ Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
 64 | 
 65 | # For Windows:
 66 | code %APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json
 67 | ```
 68 | 
 69 | 2. Add the Search Stock News server configuration:
 70 | ```json
 71 | {
 72 |   "mcpServers": {
 73 |     "search-stock-news-mcp": {
 74 |       "command": "npx",
 75 |       "args": ["-y", "search-stock-news-mcp@latest"],
 76 |       "env": {
 77 |         "TAVILY_API_KEY": "your-api-key-here"
 78 |       },
 79 |       "disabled": false,
 80 |       "autoApprove": []
 81 |     }
 82 |   }
 83 | }
 84 | ```
 85 | 
 86 | ### Configuring Cursor 🖥️
 87 | 
 88 | To set up the Search Stock News MCP server in Cursor:
 89 | 
 90 | 1. Open Cursor Settings
 91 | 2. Navigate to Features > MCP Servers
 92 | 3. Click on the "+ Add New MCP Server" button
 93 | 4. Fill out the following information:
 94 |    * **Name**: "search-stock-news-mcp"
 95 |    * **Type**: "command"
 96 |    * **Command**:
 97 |    ```bash
 98 |    env TAVILY_API_KEY=your-api-key-here npx -y search-stock-news-mcp@latest
 99 |    ```
100 | 
101 | ### Configuring Claude Desktop 🖥️
102 | 
103 | #### For macOS:
104 | ```bash
105 | touch "$HOME/Library/Application Support/Claude/claude_desktop_config.json"
106 | open -e "$HOME/Library/Application Support/Claude/claude_desktop_config.json"
107 | ```
108 | 
109 | #### For Windows:
110 | ```bash
111 | code %APPDATA%\Claude\claude_desktop_config.json
112 | ```
113 | 
114 | Add the server configuration:
115 | ```json
116 | {
117 |   "mcpServers": {
118 |     "search-stock-news-mcp": {
119 |       "command": "npx",
120 |       "args": ["-y", "search-stock-news-mcp@latest"],
121 |       "env": {
122 |         "TAVILY_API_KEY": "your-api-key-here"
123 |       }
124 |     }
125 |   }
126 | }
127 | ```
128 | 
129 | ## Usage Examples 🎯
130 | 
131 | 1. **Basic Stock News Search**:
132 | ```json
133 | {
134 |   "symbol": "AAPL",
135 |   "companyName": "Apple Inc.",
136 |   "maxResults": 10
137 | }
138 | ```
139 | 
140 | 2. **Advanced Search with Filters**:
141 | ```json
142 | {
143 |   "symbol": "TSLA",
144 |   "companyName": "Tesla Inc.",
145 |   "maxResults": 20,
146 |   "searchDepth": "advanced",
147 |   "minScore": 0.6
148 | }
149 | ```
150 | 
151 | 3. **Custom Domain Search**:
152 | ```json
153 | {
154 |   "symbol": "MSFT",
155 |   "companyName": "Microsoft Corporation",
156 |   "includeDomains": ["reuters.com", "bloomberg.com"]
157 | }
158 | ```
159 | 
160 | ## Troubleshooting 🛠️
161 | 
162 | ### Common Issues
163 | 
164 | 1. **Server Not Found**
165 |    * Verify npm installation
166 |    * Check configuration syntax
167 |    * Ensure Node.js is properly installed
168 | 
169 | 2. **API Key Issues**
170 |    * Verify your Tavily API key is valid
171 |    * Check the API key is correctly set in config
172 |    * Ensure no spaces or quotes around the API key
173 | 
174 | 3. **Search Results Issues**
175 |    * Check search parameters are within valid ranges
176 |    * Verify domain filters are correctly formatted
177 |    * Ensure company name and symbol are accurate
178 | 
179 | ## Acknowledgments ✨
180 | 
181 | * Model Context Protocol for the MCP specification
182 | * Anthropic for Claude Desktop
183 | * Tavily for the News Search API
184 | 
185 | ## License
186 | 
187 | MIT 
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
 1 | module.exports = {
 2 |   preset: "ts-jest",
 3 |   testEnvironment: "node",
 4 |   moduleFileExtensions: ["ts", "js"],
 5 |   transform: {
 6 |     "^.+\\.ts$": "ts-jest",
 7 |   },
 8 |   testMatch: ["**/__tests__/**/*.test.ts"],
 9 |   moduleNameMapper: {
10 |     "^@/(.*)$": "<rootDir>/src/$1",
11 |   },
12 | }; 
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "module": "CommonJS",
 5 |     "lib": ["ES2020"],
 6 |     "declaration": true,
 7 |     "outDir": "./dist",
 8 |     "rootDir": "./src",
 9 |     "strict": true,
10 |     "esModuleInterop": true,
11 |     "skipLibCheck": true,
12 |     "forceConsistentCasingInFileNames": true,
13 |     "moduleResolution": "node",
14 |     "resolveJsonModule": true,
15 |     "isolatedModules": true,
16 |     "noEmit": false,
17 |   },
18 |   "include": ["src/**/*"],
19 |   "exclude": ["node_modules", "dist", "**/*.test.ts"]
20 | } 
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
 1 | {
 2 |   "name": "search-stock-news-mcp",
 3 |   "version": "1.0.12",
 4 |   "description": "MCP server for searching stock news using Tavily API",
 5 |   "main": "dist/index.js",
 6 |   "types": "dist/index.d.ts",
 7 |   "type": "commonjs",
 8 |   "bin": {
 9 |     "search-stock-news-mcp": "dist/index.js"
10 |   },
11 |   "files": [
12 |     "dist",
13 |     "README.md",
14 |     "LICENSE"
15 |   ],
16 |   "scripts": {
17 |     "dev": "tsx src/index.ts",
18 |     "build": "tsc",
19 |     "start": "node dist/index.js",
20 |     "test": "jest",
21 |     "test:watch": "jest --watch",
22 |     "test:coverage": "jest --coverage",
23 |     "prepare": "npm run build"
24 |   },
25 |   "keywords": [],
26 |   "author": "Hieu TRAN @ Cognitive Stack",
27 |   "license": "MIT",
28 |   "dependencies": {
29 |     "@tavily/core": "^0.4.0",
30 |     "dotenv": "^16.0.0",
31 |     "fastmcp": "^1.27.7",
32 |     "zod": "^3.0.0"
33 |   },
34 |   "devDependencies": {
35 |     "@types/jest": "^29.0.0",
36 |     "@types/node": "^20.0.0",
37 |     "jest": "^29.0.0",
38 |     "ts-jest": "^29.0.0",
39 |     "tsx": "^3.0.0",
40 |     "typescript": "^5.0.0"
41 |   }
42 | }
43 | 
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
 1 | #!/usr/bin/env node
 2 | "use strict";
 3 | 
 4 | import { FastMCP } from "fastmcp";
 5 | import { tools } from "./services/tools";
 6 | import { Tool } from "./types/tools";
 7 | import dotenv from "dotenv";
 8 | 
 9 | // Load environment variables
10 | dotenv.config();
11 | 
12 | const server = new FastMCP({
13 |   name: "Search Stock News MCP",
14 |   version: "1.0.12",
15 |   roots: {
16 |     enabled: false
17 |   }
18 | });
19 | 
20 | // Register all tools
21 | tools.forEach((tool) => {
22 |   (server.addTool as Tool)(tool);
23 | });
24 | 
25 | // Get transport type from environment variable or default to stdio
26 | const transportType = process.env.TRANSPORT_TYPE || "stdio";
27 | 
28 | async function main() {
29 |   try {
30 |     if (transportType === "sse") {
31 |       await server.start({
32 |         transportType: "sse",
33 |         sse: {
34 |           endpoint: "/sse",
35 |           port: parseInt(process.env.PORT || "8080", 10),
36 |         },
37 |       });
38 |     } else {
39 |       await server.start({
40 |         transportType: "stdio",
41 |       });
42 |     }
43 |   } catch (error) {
44 |     console.error("Failed to start server:", error);
45 |     process.exit(1);
46 |   }
47 | }
48 | 
49 | main().catch((error) => {
50 |   console.error("Fatal error in main():", error);
51 |   process.exit(1);
52 | });
```
--------------------------------------------------------------------------------
/src/tools/general-search.ts:
--------------------------------------------------------------------------------
```typescript
 1 | "use strict";
 2 | 
 3 | import { tavily } from '@tavily/core';
 4 | import { getConfig } from '../config/env';
 5 | import { GeneralSearchResult } from '../types/tools';
 6 | 
 7 | export default async (
 8 |   query: string,
 9 |   maxResults: number = 10,
10 |   searchDepth: 'basic' | 'advanced' = 'basic',
11 |   minScore: number = 0.4
12 | ): Promise<GeneralSearchResult[]> => {
13 |   const config = getConfig();
14 |   const { apiKey } = config;
15 | 
16 |   // Initialize Tavily client
17 |   const tvly = tavily({ apiKey });
18 | 
19 |   try {
20 |     const response = await tvly.search(query, {
21 |       searchDepth,
22 |       maxResults,
23 |       includeDomains: [],
24 |       excludeDomains: []
25 |     });
26 | 
27 |     // Transform and filter the results
28 |     const filteredResults = response.results
29 |       .map((result) => ({
30 |         title: result.title,
31 |         url: result.url,
32 |         content: result.content,
33 |         publishedDate: result.publishedDate,
34 |         score: result.score,
35 |       }))
36 |       .filter(result => result.score >= minScore)
37 |       .sort((a, b) => b.score - a.score);
38 | 
39 |     return filteredResults;
40 | 
41 |   } catch (error) {
42 |     console.error(`Error performing general search: ${error}`);
43 |     throw error;
44 |   }
45 | }; 
```
--------------------------------------------------------------------------------
/src/types/tools.ts:
--------------------------------------------------------------------------------
```typescript
 1 | "use strict";
 2 | 
 3 | import { z } from "zod";
 4 | import { FastMCP } from "fastmcp";
 5 | 
 6 | export type ToolConfig = {
 7 |   name: string;
 8 |   description: string;
 9 |   parameters: z.ZodObject<any>;
10 |   execute: (args: any) => Promise<string>;
11 | };
12 | 
13 | export type Tool = FastMCP["addTool"];
14 | 
15 | export interface StockNewsConfig {
16 |   apiKey: string;
17 |   maxResults: number;
18 |   searchDepth: 'basic' | 'advanced';
19 |   minScore: number;
20 |   queryTemplates: string[];
21 |   includeDomains: string[];
22 |   excludeDomains: string[];
23 | }
24 | 
25 | export interface StockNewsResult {
26 |   title: string;
27 |   url: string;
28 |   content: string;
29 |   publishedDate: string;
30 |   score: number;
31 | }
32 | 
33 | export interface GeneralSearchResult {
34 |   title: string;
35 |   url: string;
36 |   content: string;
37 |   publishedDate: string;
38 |   score: number;
39 | }
40 | 
41 | export interface GeneralSearchConfig {
42 |   apiKey: string;
43 |   maxResults: number;
44 |   searchDepth: 'basic' | 'advanced';
45 |   minScore: number;
46 | }
47 | 
48 | export interface DramaSearchResult {
49 |   title: string;
50 |   url: string;
51 |   content: string;
52 |   publishedDate: string;
53 |   score: number;
54 |   category: 'legal' | 'scandal' | 'financial' | 'product' | 'workforce' | 'environmental' | 'security' | 'other';
55 | }
56 | 
57 | export interface DramaSearchConfig {
58 |   apiKey: string;
59 |   maxResults: number;
60 |   searchDepth: 'basic' | 'advanced';
61 |   minScore: number;
62 |   dramaQueryTemplates: string[];
63 |   dramaIncludeDomains: string[];
64 | } 
```
--------------------------------------------------------------------------------
/src/tools/search-stock-news.ts:
--------------------------------------------------------------------------------
```typescript
 1 | "use strict";
 2 | 
 3 | import { tavily } from '@tavily/core';
 4 | import { getConfig } from '../config/env';
 5 | import { StockNewsResult } from '../types/tools';
 6 | 
 7 | export default async (
 8 |   symbol: string,
 9 |   companyName: string,
10 |   days: number,
11 |   minScore: number
12 | ): Promise<Array<{
13 |   searchQuery: string;
14 |   results: StockNewsResult[];
15 | }>> => {
16 |   const config = getConfig();
17 |   const { 
18 |     apiKey, 
19 |     maxResults, 
20 |     searchDepth,
21 |     queryTemplates,
22 |     includeDomains,
23 |     excludeDomains
24 |   } = config;
25 | 
26 |   // Initialize Tavily client
27 |   const tvly = tavily({ apiKey });
28 | 
29 |   const allResults = [];
30 | 
31 |   // Process each template
32 |   for (const template of queryTemplates) {
33 |     const searchQuery = template
34 |       .replace('{symbol}', symbol)
35 |       .replace('{company_name}', companyName);
36 | 
37 |     try {
38 |       const response = await tvly.search(searchQuery, {
39 |         searchDepth,
40 |         topics: ['news'],
41 |         timeRange: "d",
42 |         days,
43 |         maxResults,
44 |         includeDomains,
45 |         excludeDomains
46 |       });
47 | 
48 |       // Transform and filter the results
49 |       const filteredResults = response.results
50 |         .map((result) => ({
51 |           title: result.title,
52 |           url: result.url,
53 |           content: result.content,
54 |           publishedDate: result.publishedDate,
55 |           score: result.score,
56 |         }))
57 |         .filter(result => result.score >= minScore)
58 |         .sort((a, b) => b.score - a.score);
59 | 
60 |       allResults.push({
61 |         searchQuery,
62 |         results: filteredResults
63 |       });
64 | 
65 |     } catch (error) {
66 |       console.error(`Error searching with template: ${template}`, error);
67 |       // Continue with other templates even if one fails
68 |       continue;
69 |     }
70 |   }
71 | 
72 |   return allResults;
73 | }; 
```
--------------------------------------------------------------------------------
/src/services/tools.ts:
--------------------------------------------------------------------------------
```typescript
 1 | "use strict";
 2 | 
 3 | import { z } from "zod";
 4 | import searchStockNews from "../tools/search-stock-news";
 5 | import generalSearch from "../tools/general-search";
 6 | import dramaSearch from "../tools/drama-search";
 7 | import { ToolConfig } from "../types/tools";
 8 | 
 9 | export const tools: ToolConfig[] = [
10 |   {
11 |     name: "search-stock-news",
12 |     description: "Search for stock-related news using Tavily API",
13 |     parameters: z.object({
14 |       symbol: z.string().describe("Stock symbol (e.g., AAPL)"),
15 |       companyName: z.string().describe("Company name (e.g., Apple Inc.)"),
16 |       maxResults: z.number().optional().describe("Maximum number of results to return"),
17 |       searchDepth: z.enum(["basic", "advanced"]).optional().describe("Search depth level"),
18 |       minScore: z.number().optional().describe("Minimum relevance score threshold")
19 |     }),
20 |     execute: async (args) => {
21 |       const results = await searchStockNews(
22 |         args.symbol,
23 |         args.companyName,
24 |         args.maxResults || 10,
25 |         args.minScore || 0.4
26 |       );
27 |       return JSON.stringify(results, null, 2);
28 |     }
29 |   },
30 |   {
31 |     name: "general-search",
32 |     description: "Perform a general web search using Tavily API",
33 |     parameters: z.object({
34 |       query: z.string().describe("Search query"),
35 |       maxResults: z.number().optional().describe("Maximum number of results to return"),
36 |       searchDepth: z.enum(["basic", "advanced"]).optional().describe("Search depth level"),
37 |       minScore: z.number().optional().describe("Minimum relevance score threshold")
38 |     }),
39 |     execute: async (args) => {
40 |       const results = await generalSearch(
41 |         args.query,
42 |         args.maxResults || 10,
43 |         args.searchDepth || "basic",
44 |         args.minScore || 0.4
45 |       );
46 |       return JSON.stringify(results, null, 2);
47 |     }
48 |   }
49 | ]; 
```
--------------------------------------------------------------------------------
/src/tools/__tests__/search-stock-news.test.ts:
--------------------------------------------------------------------------------
```typescript
 1 | import searchStockNews from "../search-stock-news";
 2 | 
 3 | jest.mock("../../config/env", () => ({
 4 |   getConfig: jest.fn().mockReturnValue({
 5 |     apiKey: "test-api-key",
 6 |     maxResults: 20,
 7 |     searchDepth: "basic",
 8 |     minScore: 0.4,
 9 |     queryTemplates: [
10 |       '{symbol} ({company_name}) latest stock price movements'
11 |     ],
12 |     includeDomains: ['https://test.com'],
13 |     excludeDomains: ['https://exclude.com']
14 |   }),
15 | }));
16 | 
17 | jest.mock('@tavily/core', () => ({
18 |   tavily: jest.fn().mockReturnValue({
19 |     search: jest.fn().mockResolvedValue({
20 |       results: [
21 |         {
22 |           title: "Test News",
23 |           url: "https://test.com/news",
24 |           content: "Test content",
25 |           publishedDate: "2024-01-01",
26 |           score: 0.8
27 |         }
28 |       ]
29 |     })
30 |   })
31 | }));
32 | 
33 | describe("searchStockNews", () => {
34 |   beforeEach(() => {
35 |     jest.clearAllMocks();
36 |   });
37 | 
38 |   it("should search for stock news with default configuration", async () => {
39 |     const result = await searchStockNews("AAPL", "Apple Inc.");
40 |     
41 |     expect(result).toHaveLength(1);
42 |     expect(result[0].template).toBe('{symbol} ({company_name}) latest stock price movements');
43 |     expect(result[0].results).toHaveLength(1);
44 |     expect(result[0].results[0].title).toBe("Test News");
45 |   });
46 | 
47 |   it("should handle custom configuration", async () => {
48 |     const customConfig = {
49 |       maxResults: 10,
50 |       searchDepth: "advanced" as const,
51 |       minScore: 0.5,
52 |       queryTemplates: ["Custom template"],
53 |       includeDomains: ["https://custom.com"],
54 |       excludeDomains: ["https://exclude-custom.com"]
55 |     };
56 | 
57 |     const result = await searchStockNews("AAPL", "Apple Inc.", customConfig);
58 |     
59 |     expect(result).toHaveLength(1);
60 |     expect(result[0].template).toBe("Custom template");
61 |   });
62 | 
63 |   it("should handle search errors gracefully", async () => {
64 |     const mockTavily = require('@tavily/core').tavily;
65 |     mockTavily.mockReturnValueOnce({
66 |       search: jest.fn().mockRejectedValue(new Error("Search failed"))
67 |     });
68 | 
69 |     const result = await searchStockNews("AAPL", "Apple Inc.");
70 |     
71 |     expect(result).toHaveLength(0);
72 |   });
73 | }); 
```
--------------------------------------------------------------------------------
/src/config/env.ts:
--------------------------------------------------------------------------------
```typescript
 1 | "use strict";
 2 | 
 3 | import { StockNewsConfig, DramaSearchConfig } from "../types/tools";
 4 | 
 5 | const parseCommaSeparatedString = (str: string | undefined, defaultValue: string[]): string[] => {
 6 |   if (!str) return defaultValue;
 7 |   return str.split(',').map(item => item.trim()).filter(Boolean);
 8 | };
 9 | 
10 | export const getConfig = (): StockNewsConfig & DramaSearchConfig => {
11 |   const apiKey = process.env.TAVILY_API_KEY;
12 |   if (!apiKey) {
13 |     throw new Error("TAVILY_API_KEY environment variable is not set");
14 |   }
15 | 
16 |   const maxResults = parseInt(process.env.MAX_RESULTS || "20", 10);
17 |   const searchDepth = (process.env.SEARCH_DEPTH || "basic") as 'basic' | 'advanced';
18 |   const minScore = parseFloat(process.env.MIN_SCORE || "0.4");
19 |   
20 |   // Stock News default templates
21 |   const defaultStockQueryTemplates = [
22 |     '{symbol} ({company_name}) latest stock price movements, trading volume analysis, and market sentiment',
23 |     '{symbol} ({company_name}) earnings reports, revenue guidance, and financial metrics',
24 |     '{symbol} ({company_name}) company news, regulatory filings, and material events'
25 |   ];
26 |   
27 |   // Drama Search default templates
28 |   const defaultDramaQueryTemplates = [
29 |     "{company_name} ({symbol}) controversy",
30 |     "{company_name} ({symbol}) scandal",
31 |     "{company_name} ({symbol}) lawsuit",
32 |     "{company_name} ({symbol}) legal issues",
33 |     "{company_name} ({symbol}) controversy latest news",
34 |     "{company_name} ({symbol}) scandal latest news",
35 |     "{company_name} ({symbol}) drama",
36 |     "{company_name} ({symbol}) problems",
37 |     "{company_name} ({symbol}) issues",
38 |     "{company_name} ({symbol}) negative news"
39 |   ];
40 |   
41 |   const defaultIncludeDomains = [
42 |     'https://cafef.vn',
43 |     'https://nguoiquansat.vn'
44 |   ];
45 |   
46 |   const defaultExcludeDomains = [
47 |     'rs.nguoiquansat.vn',
48 |     'en.vneconomy.vn',
49 |     'en.vietstock.vn'
50 |   ];
51 | 
52 |   // Drama Search default domains
53 |   const defaultDramaIncludeDomains = [
54 |     'https://reuters.com',
55 |     'https://bloomberg.com',
56 |     'https://wsj.com',
57 |     'https://ft.com',
58 |     'https://cnbc.com',
59 |     'https://cafef.vn',
60 |     'https://nguoiquansat.vn',
61 |     'https://voz.vn/f/%C4%90iem-bao.33/'
62 |   ];
63 | 
64 |   return {
65 |     apiKey,
66 |     maxResults,
67 |     searchDepth,
68 |     minScore,
69 |     // Stock News config
70 |     queryTemplates: parseCommaSeparatedString(process.env.QUERY_TEMPLATES, defaultStockQueryTemplates),
71 |     includeDomains: parseCommaSeparatedString(process.env.INCLUDE_DOMAINS, defaultIncludeDomains),
72 |     excludeDomains: parseCommaSeparatedString(process.env.EXCLUDE_DOMAINS, defaultExcludeDomains),
73 |     // Drama Search config
74 |     dramaQueryTemplates: parseCommaSeparatedString(process.env.DRAMA_QUERY_TEMPLATES, defaultDramaQueryTemplates),
75 |     dramaIncludeDomains: parseCommaSeparatedString(process.env.DRAMA_INCLUDE_DOMAINS, defaultDramaIncludeDomains)
76 |   };
77 | }; 
```
--------------------------------------------------------------------------------
/src/tools/drama-search.ts:
--------------------------------------------------------------------------------
```typescript
 1 | "use strict";
 2 | 
 3 | import { tavily } from '@tavily/core';
 4 | import { getConfig } from '../config/env';
 5 | import { DramaSearchResult } from '../types/tools';
 6 | 
 7 | export default async (
 8 |   symbol: string,
 9 |   companyName: string,
10 |   maxResults: number = 10,
11 |   searchDepth: 'basic' | 'advanced' = 'basic',
12 |   minScore: number = 0.4
13 | ): Promise<Array<{
14 |   searchQuery: string;
15 |   results: DramaSearchResult[];
16 | }>> => {
17 |   const config = getConfig();
18 |   const { apiKey, dramaQueryTemplates, dramaIncludeDomains } = config;
19 | 
20 |   // Initialize Tavily client
21 |   const tvly = tavily({ apiKey });
22 | 
23 |   const allResults = [];
24 | 
25 |   // Process each template
26 |   for (const template of dramaQueryTemplates) {
27 |     const searchQuery = template
28 |       .replace('{symbol}', symbol)
29 |       .replace('{company_name}', companyName);
30 | 
31 |     try {
32 |       const response = await tvly.search(searchQuery, {
33 |         searchDepth,
34 |         maxResults,
35 |         includeDomains: dramaIncludeDomains,
36 |         excludeDomains: []
37 |       });
38 | 
39 |       // Transform and filter the results
40 |       const filteredResults = response.results
41 |         .map((result) => ({
42 |           title: result.title,
43 |           url: result.url,
44 |           content: result.content,
45 |           publishedDate: result.publishedDate,
46 |           score: result.score,
47 |           category: determineDramaCategory(result.title, result.content)
48 |         }))
49 |         .filter(result => result.score >= minScore)
50 |         .sort((a, b) => b.score - a.score);
51 | 
52 |       allResults.push({
53 |         searchQuery,
54 |         results: filteredResults
55 |       });
56 | 
57 |     } catch (error) {
58 |       console.error(`Error searching with template: ${template}`, error);
59 |       // Continue with other templates even if one fails
60 |       continue;
61 |     }
62 |   }
63 | 
64 |   return allResults;
65 | };
66 | 
67 | // Helper function to categorize drama/controversy type
68 | function determineDramaCategory(title: string, content: string): "legal" | "scandal" | "financial" | "product" | "workforce" | "environmental" | "security" | "other" {
69 |   const text = (title + ' ' + content).toLowerCase();
70 |   
71 |   if (text.includes('lawsuit') || text.includes('legal') || text.includes('court')) {
72 |     return 'legal';
73 |   }
74 |   if (text.includes('scandal') || text.includes('controversy')) {
75 |     return 'scandal';
76 |   }
77 |   if (text.includes('financial') || text.includes('earnings') || text.includes('stock')) {
78 |     return 'financial';
79 |   }
80 |   if (text.includes('product') || text.includes('service') || text.includes('quality')) {
81 |     return 'product';
82 |   }
83 |   if (text.includes('employee') || text.includes('workforce') || text.includes('staff')) {
84 |     return 'workforce';
85 |   }
86 |   if (text.includes('environmental') || text.includes('climate') || text.includes('pollution')) {
87 |     return 'environmental';
88 |   }
89 |   if (text.includes('security') || text.includes('breach') || text.includes('hack')) {
90 |     return 'security';
91 |   }
92 |   
93 |   return 'other';
94 | } 
```