#
tokens: 14837/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .env.sample
├── .github
│   ├── FUNDING.yml
│   └── workflows
│       ├── docker.yaml
│       └── publish.yaml
├── .gitignore
├── .python-version
├── Dockerfile
├── images
│   ├── image-1.png
│   └── image-2.png
├── LICENSE
├── pyproject.toml
├── README.md
├── smithery.yaml
├── src
│   └── ssi_stock_mcp_server
│       ├── __init__.py
│       └── server.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.13
2 | 
```

--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------

```
1 | FC_DATA_URL=https://fc-data.ssi.com.vn/
2 | FC_DATA_CONSUMER_ID=id
3 | FC_DATA_CONSUMER_SECRET=secret_key
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Python
 2 | __pycache__/
 3 | *.py[cod]
 4 | *$py.class
 5 | *.so
 6 | .Python
 7 | build/
 8 | develop-eggs/
 9 | dist/
10 | downloads/
11 | eggs/
12 | .eggs/
13 | lib/
14 | lib64/
15 | parts/
16 | sdist/
17 | var/
18 | wheels/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 | 
23 | # Virtual Environment
24 | .env
25 | .venv
26 | env/
27 | venv/
28 | ENV/
29 | 
30 | # IDE
31 | .idea/
32 | .vscode/
33 | *.swp
34 | *.swo
35 | 
36 | # Testing
37 | .coverage
38 | htmlcov/
39 | .pytest_cache/
40 | .tox/
41 | 
42 | # Distribution
43 | *.tar.gz
44 | *.whl
45 | 
46 | # Logs
47 | *.log
48 | .info.json
49 | # Local development
50 | .DS_Store
51 | 
52 | # Environment variables
53 | .env
54 | 
55 | # Database
56 | *.db
57 | *.sqlite3
58 | 
59 | # Unit test / coverage reports
60 | htmlcov/
61 | .tox/
62 | .coverage
63 | .coverage.*
64 | .cache
65 | nosetests.xml
66 | coverage.xml
67 | *.cover
68 | .hypothesis/
69 | .pytest_cache/
70 | 
71 | # mypy
72 | .mypy_cache/
73 | 
74 | # Jupyter Notebook
75 | .ipynb_checkpoints 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/archiephan78-ssi-stock-mcp-server-badge.png)](https://mseep.ai/app/archiephan78-ssi-stock-mcp-server)
  2 | 
  3 | <div align="center">
  4 |     <h1>SSI Stock Data MCP </h1>
  5 |     <p>
  6 |         <a href="https://github.com/archiephan78/ssi-stock-mcp-server/blob/main/LICENSE">
  7 |             <img alt="GitHub license" src="https://img.shields.io/github/license/archiephan78/ssi-stock-mcp-server?style=for-the-badge">
  8 |         </a>
  9 |     <a href="https://github.com/archiephan78/ssi-stock-mcp-server/stargazers">
 10 |         <img alt="GitHub stars" src="https://img.shields.io/github/stars/archiephan78/ssi-stock-mcp-server?style=for-the-badge">
 11 |     </a>
 12 | </div>
 13 | 
 14 | ## Table of Contents
 15 | - [Table of Contents](#table-of-contents)
 16 | - [1. Introduction](#1-introduction)
 17 | - [2. Features](#2-features)
 18 | - [3. Quickstart](#3-quickstart)
 19 |   - [3.1. Prerequisites](#31-prerequisites)
 20 |   - [3.2. Local Run](#32-local-run)
 21 |   - [3.3. Docker Run](#33-docker-run)
 22 | - [4. Tools](#4-tools)
 23 | - [5. Development](#5-development)
 24 | - [6. License](#6-license)
 25 | 
 26 | ## 1. Introduction
 27 | 
 28 | SSI Stock Data MCP is a [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server for get  VietNam stock intraday data. It enables AI assistants and tools to query intraday data using [SSI FastConnect API](https://guide.ssi.com.vn/ssi-products/tieng-viet/fastconnect-data) programmatically and securely.
 29 | 
 30 | ## 2. Features
 31 | 
 32 | - [x] List of stock codes by exchange
 33 | - [x] Retrieve detailed information of a stock code
 34 | - [x] Retrieve the list of stock codes in an index basket
 35 | - [x] Retrieve the list of index codes
 36 | - [x] Retrieve the open, high, low, close, volume, and value information of a stock code by each tick data
 37 | - [x] Retrieve the open, high, low, close, volume, and value information of a stock code by day
 38 | - [x] Retrieve the daily trading results of the composite index
 39 | - [x] Retrieve the daily trading information of a stock code
 40 | - [x] Docker containerization support
 41 | - [ ] Support get realtime data via streaming adapter (planning)
 42 | - [ ] Support order management and trading via MCP (future consideration)
 43 | 
 44 | ## 3. Quickstart
 45 | 
 46 | ### 3.1. Prerequisites
 47 | 
 48 | - Python 3.12+
 49 | - [uv](https://github.com/astral-sh/uv) (for fast dependency management).
 50 | - Docker (optional, for containerized deployment).
 51 | - Ensure you register for SSI FastConnect service before running this MCP server. You can register at [SSI FastConnect](https://guide.ssi.com.vn/ssi-products/tieng-viet/dang-ky-dich-vu) to get your consumer ID and secret.
 52 | 
 53 | ### Installing via Smithery
 54 | 
 55 | To install SSI Stock MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@archiephan78/ssi-stock-mcp-server):
 56 | 
 57 | ```bash
 58 | npx -y @smithery/cli install @archiephan78/ssi-stock-mcp-server --client claude
 59 | ```
 60 | 
 61 | ### 3.2. Local Run
 62 | 
 63 | - Clone the repository:
 64 | 
 65 | ```bash
 66 | # Clone the repository
 67 | $ git clone https://github.com/archiephan78/ssi-stock-mcp-server.git
 68 | ```
 69 | 
 70 | - Configure the environment variables 
 71 | 
 72 | ```shell
 73 | # Set environment variables (see .env.sample)
 74 | FC_DATA_URL=https://fc-data.ssi.com.vn/ #optional
 75 | FC_DATA_AUTH_TYPE=Bearer #optional
 76 | FC_DATA_CONSUMER_ID=your_consumer_id 
 77 | FC_DATA_CONSUMER_SECRET=your_consumer_secret  
 78 | ```
 79 | 
 80 | - Add the server configuration to your client configuration file. For example, for Claude Desktop:
 81 | 
 82 | ```json
 83 | {
 84 |   "mcpServers": {
 85 |     "SSIStockMCPServer": {
 86 |       "command": "uv",
 87 |       "args": ["--directory", "full-path", "run", "ssi-stock-mcp-server"],
 88 |       "env": {
 89 |         "FC_DATA_CONSUMER_ID": "id",
 90 |         "FC_DATA_CONSUMER_SECRET": "id",
 91 |         "FC_DATA_URL": "https://fc-data.ssi.com.vn/",
 92 |         "FC_DATA_AUTH_TYPE": "Bearer"
 93 |       }
 94 |     }
 95 |   }
 96 | }
 97 | ```
 98 | 
 99 | - Restart Claude Desktop to load new configuration.
100 | - You can now ask Claude to interact with data using natual language:
101 |   - "chỉ số VN30 hôm nay có gì hot không"
102 |   - "get volume room ngoại đã bán của SSI hôm nay"
103 |   - "so sánh vol của SSI với VND trong ngày hôm nay"
104 |   - "total matchvol của SSI trong 1 tuần trở lại đây"
105 | 
106 | ![](./images/image-1.png)
107 | 
108 | ![](./images/image-2.png)
109 | 
110 | ### 3.3. Docker Run
111 | 
112 | - Run it with pre-built image (or you can build it yourself):
113 | 
114 | ```bash
115 | $ docker run -p 8000:8000 
116 |              -e FC_DATA_CONSUMER_ID=id 
117 |              -e FC_DATA_CONSUMER_SECRET=id ghcr.io/archiephan78/ssi-stock-mcp-server
118 | ```
119 | 
120 | - Running with Docker in Claude Desktop:
121 | 
122 | ```json
123 | {
124 |   "mcpServers": {
125 |     "SSIStockMCPServer": {
126 |       "command": "docker",
127 |       "args": [
128 |         "run",
129 |         "--rm",
130 |         "-i",
131 |         "-e", "FC_DATA_CONSUMER_ID",
132 |         "-e", "FC_DATA_CONSUMER_SECRET",
133 |         "ghcr.io/archiephan78/ssi-stock-mcp-server:latest"
134 |       ],
135 |       "env": {
136 |         "FC_DATA_CONSUMER_ID": "your_username",
137 |         "FC_DATA_CONSUMER_SECRET": "your_password"
138 |       }
139 |     }
140 |   }
141 | }
142 | ```
143 | 
144 | This configuration passes the environment variables from Claude Desktop to the Docker container by using the `-e` flag with just the variable name, and providing the actual values in the `env` object.
145 | 
146 | ## 4. Tools
147 | 
148 | The MCP server exposes tools:
149 | - Get securities list: `get_securities_list()`
150 | - Get securities detail: `get_securities_details()`
151 | - Get index: `get_index_components()`
152 | - Get list index: `get_index_list()`
153 | - Get daily open,high,low,close: `get_daily_ohlc()`
154 | - Get intraday open,high,low,close: `get_intraday_ohlc()`
155 | - Get daily index: `get_daily_index()`
156 | - Get stock price: `get_stock_price()`
157 | 
158 | See [src/ssi_stock_mcp_server/server.py](src/ssi_stock_mcp_server/server.py) for full API details.
159 | 
160 | ## 5. Development
161 | 
162 | Contributions are welcome! Please open an issue or submit a pull request if you have any suggestions or improvements.
163 | 
164 | This project uses [uv](https://github.com/astral-sh/uv) to manage dependencies. Install uv following the instructions for your platform.
165 | 
166 | ```bash
167 | # Clone the repository
168 | $ git clone https://github.com/archiephan78/ssi-stock-mcp-server.git
169 | $ uv venv
170 | $ source .venv/bin/activate  # On Unix/macOS
171 | $ .venv\Scripts\activate     # On Windows
172 | $ uv pip install -e .
173 | # run test
174 | $ pytest
175 | ```
176 | 
177 | ## 6. License
178 | 
179 | [Apache 2.0](LICENSE)
180 | 
181 | ## Contact / Support
182 | 
183 | - Please open an issue on GitHub if you encounter any problems or need support.
184 | - Email: n/a
185 | 
```

--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------

```yaml
1 | github: [archiephan78]
2 | buy_me_a_coffee: archiephan78
3 | 
```

--------------------------------------------------------------------------------
/src/ssi_stock_mcp_server/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | SSI Stock MCP Server
3 | A Message Control Protocol (MCP) server for SSI Stock integration.
4 | """
5 | 
6 | __version__ = "1.0.0"
7 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/build/project-config
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - consumerID
10 |       - consumerSecret
11 |     properties:
12 |       consumerID:
13 |         type: string
14 |         description: Consumer ID of the SSI Stock API server
15 |       consumerSecret:
16 |         type: string
17 |         description: Consumer secret of the SSI Stock API server
18 |   commandFunction:
19 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
20 |     |-
21 |     (config) => ({ command: 'ssi-stock-mcp-server', env: { FC_DATA_CONSUMER_ID: config.consumerID, FC_DATA_CONSUMER_SECRET: config.consumerSecret } })
22 |   exampleConfig:
23 |     consumerID: your_consumer_id
24 |     consumerSecret: your_api_key
25 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "ssi_stock_mcp_server"
 3 | version = "1.0.0"
 4 | description = "MCP Server for SSI Stock integration"
 5 | readme = "README.md"
 6 | requires-python = ">=3.12"
 7 | dependencies = ["mcp[cli]>=1.8.1", "requests>=2.32.3", "ssi_fc_data>=2.2.2"]
 8 | 
 9 | [project.scripts]
10 | ssi-stock-mcp-server = "ssi_stock_mcp_server.server:run_server"
11 | 
12 | [tool.setuptools]
13 | packages = ["ssi_stock_mcp_server"]
14 | package-dir = { "" = "src" }
15 | 
16 | [build-system]
17 | requires = ["setuptools>=61.0"]
18 | build-backend = "setuptools.build_meta"
19 | 
20 | [dependency-groups]
21 | dev = [
22 |     "pytest-cov>=6.1.1",
23 |     "pytest>=8.3.5",
24 |     "pytest-asyncio>=0.26.0",
25 | ]
26 | 
27 | [tool.pytest.ini_options]
28 | testpaths = ["tests"]
29 | python_files = "test_*.py"
30 | python_functions = "test_*"
31 | python_classes = "Test*"
32 | addopts = "--cov=src --cov-report=term-missing"
33 | 
34 | [tool.coverage.run]
35 | source = ["src/ssi_stock_mcp_server"]
36 | omit = ["*/__pycache__/*", "*/tests/*", "*/.venv/*", "*/venv/*"]
37 | 
38 | [tool.coverage.report]
39 | exclude_lines = [
40 |     "pragma: no cover",
41 |     "def __repr__",
42 |     "if self.debug:",
43 |     "raise NotImplementedError",
44 |     "if __name__ == .__main__.:",
45 |     "pass",
46 |     "raise ImportError",
47 | ]
48 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config
 2 | FROM python:3.12-slim-bookworm AS builder
 3 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
 4 | WORKDIR /app
 5 | ENV UV_COMPILE_BYTECODE=1
 6 | COPY pyproject.toml uv.lock ./
 7 | COPY src ./src/
 8 | RUN uv venv && uv pip install --no-cache-dir -e .
 9 | 
10 | FROM python:3.12-slim-bookworm
11 | WORKDIR /app
12 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
13 | RUN groupadd -r app && useradd -r -g app app
14 | COPY --from=builder /app/.venv /app/.venv
15 | COPY --from=builder /app/src /app/src
16 | COPY pyproject.toml ./
17 | ENV PATH="/app/.venv/bin:$PATH" \
18 |     PYTHONUNBUFFERED=1 \
19 |     PYTHONDONTWRITEBYTECODE=1 \
20 |     PYTHONPATH="/app" \
21 |     PYTHONFAULTHANDLER=1
22 | USER app
23 | EXPOSE 8000
24 | HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
25 |     CMD curl -f http://localhost:8000/health || exit 1
26 | CMD ["/app/.venv/bin/ssi-stock-mcp-server"]
27 | 
28 | LABEL org.opencontainers.image.title="SSI Stock intraday data MCP Server" \
29 |     org.opencontainers.image.version="0.0.1" \
30 |     org.opencontainers.image.source="https://github.com/archiephan78/ssi-stock-mcp-server" \
31 |     org.opencontainers.image.licenses="Apache 2" \
32 |     org.opencontainers.image.authors="Chung Phan" \
33 |     org.opencontainers.image.url="https://github.com/archiephan78/ssi-stock-mcp-server" \
34 |     org.opencontainers.image.documentation="https://github.com/archiephan78/ssi-stock-mcp-server#readme"
```

--------------------------------------------------------------------------------
/.github/workflows/docker.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Build and Push Docker Image
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: ["main"]
 6 |     tags: ["v*"]
 7 |   pull_request:
 8 |     branches: ["main"]
 9 | 
10 | env:
11 |   REGISTRY: ghcr.io
12 |   IMAGE_NAME: ${{ github.repository }}
13 | 
14 | jobs:
15 |   build-and-push:
16 |     name: Build and Push Docker Image
17 |     runs-on: ubuntu-latest
18 |     permissions:
19 |       contents: read
20 |       packages: write
21 | 
22 |     steps:
23 |       - name: Checkout repository
24 |         uses: actions/checkout@v4
25 | 
26 |       - name: Set up Docker Buildx
27 |         uses: docker/setup-buildx-action@v3
28 | 
29 |       - name: Log in to the Container registry
30 |         if: github.event_name != 'pull_request'
31 |         uses: docker/login-action@v3
32 |         with:
33 |           registry: ${{ env.REGISTRY }}
34 |           username: ${{ github.actor }}
35 |           password: ${{ secrets.GITHUB_TOKEN }}
36 | 
37 |       - name: Extract metadata (tags, labels) for Docker
38 |         id: meta
39 |         uses: docker/metadata-action@v5
40 |         with:
41 |           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
42 |           tags: |
43 |             type=ref,event=branch
44 |             type=ref,event=pr
45 |             type=semver,pattern={{version}}
46 |             type=semver,pattern={{major}}.{{minor}}
47 |             type=semver,pattern={{major}}
48 |             type=sha,format=long
49 | 
50 |       - name: Build and push Docker image
51 |         uses: docker/build-push-action@v5
52 |         with:
53 |           context: .
54 |           push: ${{ github.event_name != 'pull_request' }}
55 |           tags: ${{ steps.meta.outputs.tags }}
56 |           labels: ${{ steps.meta.outputs.labels }}
57 |           cache-from: type=gha
58 |           cache-to: type=gha,mode=max
59 | 
```

--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
 2 | 
 3 | on: push
 4 | 
 5 | jobs:
 6 |   build:
 7 |     name: Build distribution 📦
 8 |     runs-on: ubuntu-latest
 9 | 
10 |     steps:
11 |       - uses: actions/checkout@v4
12 |         with:
13 |           persist-credentials: false
14 |       - name: Set up Python
15 |         uses: actions/setup-python@v5
16 |         with:
17 |           python-version: "3.12"
18 |       - name: Install pypa/build
19 |         run: >-
20 |           python3 -m
21 |           pip install
22 |           build
23 |           --user
24 |       - name: Build a binary wheel and a source tarball
25 |         run: python3 -m build
26 |       - name: Store the distribution packages
27 |         uses: actions/upload-artifact@v4
28 |         with:
29 |           name: python-package-distributions
30 |           path: dist/
31 | 
32 |   publish-to-pypi:
33 |     name: >-
34 |       Publish Python 🐍 distribution 📦 to PyPI
35 |     if: startsWith(github.ref, 'refs/tags/')
36 |     needs:
37 |       - build
38 |     runs-on: ubuntu-latest
39 |     environment:
40 |       name: pypi
41 |       url: https://pypi.org/p/alertmanager_mcp_server
42 |     permissions:
43 |       id-token: write
44 | 
45 |     steps:
46 |       - name: Download all the dists
47 |         uses: actions/download-artifact@v4
48 |         with:
49 |           name: python-package-distributions
50 |           path: dist/
51 |       - name: Publish distribution 📦 to PyPI
52 |         uses: pypa/gh-action-pypi-publish@release/v1
53 | 
54 |   github-release:
55 |     name: >-
56 |       Sign the Python 🐍 distribution 📦 with Sigstore
57 |       and upload them to GitHub Release
58 |     needs:
59 |       - publish-to-pypi
60 |     runs-on: ubuntu-latest
61 | 
62 |     permissions:
63 |       contents: write
64 |       id-token: write
65 | 
66 |     steps:
67 |       - name: Download all the dists
68 |         uses: actions/download-artifact@v4
69 |         with:
70 |           name: python-package-distributions
71 |           path: dist/
72 |       - name: Sign the dists with Sigstore
73 |         uses: sigstore/[email protected]
74 |         with:
75 |           inputs: >-
76 |             ./dist/*.tar.gz
77 |             ./dist/*.whl
78 |       - name: Create GitHub Release
79 |         env:
80 |           GITHUB_TOKEN: ${{ github.token }}
81 |         run: >-
82 |           gh release create
83 |           "$GITHUB_REF_NAME"
84 |           --repo "$GITHUB_REPOSITORY"
85 |           --notes ""
86 |       - name: Upload artifact signatures to GitHub Release
87 |         env:
88 |           GITHUB_TOKEN: ${{ github.token }}
89 |         run: >-
90 |           gh release upload
91 |           "$GITHUB_REF_NAME" dist/**
92 |           --repo "$GITHUB_REPOSITORY"
93 | 
```

--------------------------------------------------------------------------------
/src/ssi_stock_mcp_server/server.py:
--------------------------------------------------------------------------------

```python
  1 | import logging
  2 | import os
  3 | from typing import Dict
  4 | from dataclasses import dataclass
  5 | from mcp.server.fastmcp import FastMCP
  6 | from ssi_fc_data import fc_md_client, model
  7 | import dotenv
  8 | 
  9 | @dataclass
 10 | class SSIAuthConfig:
 11 |     url: str
 12 |     auth_type: str
 13 |     consumerID: str
 14 |     consumerSecret: str
 15 | 
 16 | config = SSIAuthConfig(
 17 |     url=os.environ.get("FC_DATA_URL", "https://fc-data.ssi.com.vn/"),
 18 |     auth_type=os.environ.get("FC_DATA_AUTH_TYPE", "Bearer"),
 19 |     consumerID=os.environ.get("FC_DATA_CONSUMER_ID", ""),
 20 |     consumerSecret=os.environ.get("FC_DATA_CONSUMER_SECRET", ""),
 21 | )
 22 | 
 23 | logging.basicConfig(
 24 |     level=logging.INFO,
 25 |     format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
 26 | )
 27 | logger = logging.getLogger(__name__)
 28 | 
 29 | VALID_MARKETS = ["HOSE", "HNX", "UPCOM", "DER"]
 30 | mcp = FastMCP("SSI Stock Market Data MCP Server")
 31 | 
 32 | def get_fc_client():
 33 |     client = fc_md_client.MarketDataClient(config)
 34 |     return client
 35 | 
 36 | client = get_fc_client()
 37 | 
 38 | def _validate_date_params(symbol: str, from_date: str, to_date: str):
 39 |     if not all([symbol, from_date, to_date]):
 40 |         raise ValueError("symbol, from_date, and to_date are required")
 41 | 
 42 | def _process_securities_response(response: Dict) -> Dict:
 43 |     """
 44 |     Process and validate the securities API response.
 45 |     
 46 |     Args:
 47 |         response (Dict): The raw response from the API
 48 |         
 49 |     Returns:
 50 |         Dict: Processed response with standardized fields
 51 |         
 52 |     Raises:
 53 |         ValueError: If the response format is invalid
 54 |     """
 55 |     if not isinstance(response, dict):
 56 |         raise ValueError("Invalid response format")
 57 |     if response.get("status") != 200:
 58 |         logger.warning(f"API returned non-success status: {response.get('status')}")
 59 |     if "data" not in response or not isinstance(response["data"], list):
 60 |         response["data"] = []
 61 |     return response
 62 |     
 63 | @mcp.tool(
 64 |     description="Get list of securities from a specific market (HOSE/HNX/UPCOM/DER)"
 65 | )
 66 | async def get_securities_list(market: str, page: int = 1, size: int = 100) -> Dict:
 67 |     
 68 |     """
 69 |     Get list of securities from a specified market.
 70 |     
 71 |     Args:
 72 |         market (str): Market code (HOSE/HNX/UPCOM/DER)
 73 |         page (int, optional): Page number for pagination. Defaults to 1.
 74 |         size (int, optional): Number of records per page. Defaults to 100.
 75 |         
 76 |     Returns:
 77 |         Dict: A dictionary containing securities information with the following structure:
 78 |             {
 79 |                 "message": str,    # Response message from the API
 80 |                 "status": int,     # Status code (200 for success)
 81 |                 "totalRecord": int, # Total number of records available
 82 |                 "data": [          # List of securities
 83 |                     {
 84 |                         "market": str,      # Market code (HOSE/HNX/UPCOM/DER)
 85 |                         "symbol": str,      # Security code/ticker
 86 |                         "StockName": str,   # Company name in Vietnamese
 87 |                         "StockEnName": str, # Company name in English
 88 |                         # ... possibly other fields
 89 |                     },
 90 |                     # ... more securities
 91 |                 ]
 92 |             }
 93 |             
 94 |     Raises:
 95 |         ValueError: If the market is not one of the valid markets.
 96 |     """
 97 |     if market not in VALID_MARKETS:
 98 |         raise ValueError("Market must be one of: HOSE, HNX, UPCOM, DER")
 99 |     req = model.securities(market, page, size)
100 |     response = client.securities(config, req)
101 |     return _process_securities_response(response)
102 | 
103 | @mcp.tool(
104 |     description="Get detailed information about a specific security"
105 | )
106 | async def get_securities_details(market: str, symbol: str, page: int = 1, size: int = 100) -> Dict:
107 |     
108 |     """
109 |     Get detailed information about a specific security.
110 |     
111 |     Args:
112 |         market (str): Market code (HOSE/HNX/UPCOM/DER)
113 |         symbol (str): Security symbol/ticker
114 |         page (int, optional): Page number for pagination. Defaults to 1.
115 |         size (int, optional): Number of records per page. Defaults to 100.
116 |         
117 |     Returns:
118 |         Dict: A dictionary containing detailed securities information with the following structure:
119 |             {
120 |                 "message": str,      # Response message from the API
121 |                 "status": int,       # Status code (200 for success)
122 |                 "totalRecord": int,  # Total number of records available
123 |                 "data": {
124 |                     "RType": str,               # Type indicator, typically "y"
125 |                     "ReportDate": str,          # Report date in format dd/mm/yyyy
126 |                     "TotalNoSym": int,          # Total number of securities returned
127 |                     "repeatedinfoList": [       # List of security details
128 |                         {
129 |                             "Isin": str,              # ISIN code of the security
130 |                             "Symbol": str,            # Trading symbol listed on exchanges
131 |                             "SymbolName": str,        # Name of the security in Vietnamese
132 |                             "SymbolEngName": str,     # Name of the security in English
133 |                             "SecType": str,           # Security type (ST: Stock, CW: Covered Warrant, 
134 |                                                         # FU: Futures, EF: ETF, BO: BOND, OF: OEF, MF: Mutual Fund)
135 |                             "Exchange": str,          # Exchange where the security is traded
136 |                                                         # (HOSE, HNX, HNXBOND, UPCOM, DER)
137 |                             "Issuer": str,            # Security issuer
138 |                             "LotSize": str,           # Trading lot size of the security
139 |                             "IssueDate": str,         # Issue date
140 |                             "MaturityDate": str,      # Maturity date
141 |                             "FirstTradingDate": str,  # First trading date
142 |                             "LastTradingDate": str,   # Last trading date
143 |                             "ContractMultiplier": str, # Contract multiplier
144 |                             "SettlMethod": int,       # Settlement method
145 |                             "Underlying": str,        # Underlying security
146 |                             "PutOrCall": str,         # Option type
147 |                             "ExercisePrice": str,     # Exercise price (for options, CW)
148 |                             "ExerciseStyle": int,     # Exercise style (for CW, options)
149 |                             "ExcerciseRatio": str,    # Exercise ratio (for CW, options)
150 |                             "ListedShare": str,       # Number of listed shares
151 |                             "TickPrice1": float,      # Starting price range 1 for tick rule
152 |                             "TickIncrement1": float,  # Tick increment for price range 1
153 |                             "TickPrice2": float,      # Starting price range 2 for tick rule
154 |                             "TickIncrement2": float,  # Tick increment for price range 2
155 |                             "TickPrice3": float,      # Starting price range 3 for tick rule
156 |                             "TickIncrement3": float,  # Tick increment for price range 3
157 |                             "TickPrice4": float,      # Starting price range 4 for tick rule
158 |                             "TickIncrement4": float,  # Tick increment for price range 4
159 |                         },
160 |                         # ... more securities details
161 |                     ]
162 |                 }
163 |             }
164 |             
165 |     Raises:
166 |         ValueError: If the symbol is not provided.
167 |     """
168 |     if not symbol:
169 |         raise ValueError("Symbol is required")
170 |     if market not in VALID_MARKETS:
171 |         raise ValueError("Market must be one of: HOSE, HNX, UPCOM, DER")
172 |     req = model.securities_details(market, symbol, page, size)
173 |     response = client.securities_details(config, req)
174 |     return _process_securities_details_response(response)
175 | 
176 | def _process_securities_details_response(response: Dict) -> Dict:
177 |     """
178 |     Process and validate the securities details API response.
179 |     
180 |     Args:
181 |         response (Dict): The raw response from the API
182 |         
183 |     Returns:
184 |         Dict: Processed response with standardized fields
185 |         
186 |     Raises:
187 |         ValueError: If the response format is invalid
188 |     """
189 |     if not isinstance(response, dict):
190 |         raise ValueError("Invalid response format")
191 |     if response.get("status") != 200:
192 |         logger.warning(f"API returned non-success status: {response.get('status')}")
193 |     if "data" not in response:
194 |         response["data"] = {"repeatedinfoList": []}
195 |     elif not isinstance(response["data"], dict):
196 |         logger.warning("Data field is not a dictionary, normalizing")
197 |         old_data = response["data"]
198 |         response["data"] = {"repeatedinfoList": []}
199 |         if isinstance(old_data, list) and len(old_data) > 0:
200 |             response["data"]["repeatedinfoList"] = old_data
201 |     if "repeatedinfoList" not in response["data"] or not isinstance(response["data"]["repeatedinfoList"], list):
202 |         response["data"]["repeatedinfoList"] = []
203 |         
204 |     return response
205 | 
206 | @mcp.tool(
207 |     description="Get components of a specific index"
208 | )
209 | async def get_index_components(index: str = "vn100", page: int = 1, size: int = 100) -> Dict:
210 |     
211 |     """
212 |     Get components (constituent stocks) of a specific index.
213 |     
214 |     Args:
215 |         index (str, optional): Index code. Defaults to "vn100".
216 |         page (int, optional): Page number for pagination. Defaults to 1.
217 |         size (int, optional): Number of records per page. Defaults to 100.
218 |         
219 |     Returns:
220 |         Dict: A dictionary containing index components with the following structure:
221 |             {
222 |                 "message": str,      # Response message from the API
223 |                 "status": int,       # Status code (200 for success)
224 |                 "totalRecord": int,  # Total number of records available
225 |                 "data": [
226 |                     {
227 |                         "IndexCode": str,       # Index code identifier
228 |                         "IndexName": str,       # Name of the index
229 |                         "Exchange": str,        # Exchange where index is listed (HOSE|HNX)
230 |                         "TotalSymbolNo": int,   # Total number of securities in the index
231 |                         "IndexComponent": [     # List of component securities
232 |                             {
233 |                                 "Isin": str,        # ISIN code of the security
234 |                                 "StockSymbol": str, # Stock symbol/ticker
235 |                             },
236 |                             # ... more component securities
237 |                         ]
238 |                     },
239 |                     # ... possibly more indexes
240 |                 ]
241 |             }
242 |             
243 |     Raises:
244 |         ValueError: If the response format is invalid
245 |     """
246 |     if not index:
247 |         raise ValueError("Index code is required")
248 |     req = model.index_components(index, page, size)
249 |     response = client.index_components(config, req)
250 |     return _process_index_components_response(response)
251 | 
252 | def _process_index_components_response(response: Dict) -> Dict:
253 |     """
254 |     Process and validate the index components API response.
255 |     
256 |     Args:
257 |         response (Dict): The raw response from the API
258 |         
259 |     Returns:
260 |         Dict: Processed response with standardized fields
261 |         
262 |     Raises:
263 |         ValueError: If the response format is invalid
264 |     """
265 |     if response.get("status") != 200:
266 |         logger.warning(f"API returned non-success status: {response.get('status')}")
267 |     if "data" not in response or not isinstance(response["data"], list):
268 |         response["data"] = []
269 |     for index_data in response["data"]:
270 |         if "IndexComponent" not in index_data or not isinstance(index_data["IndexComponent"], list):
271 |             index_data["IndexComponent"] = []
272 |         if "IndexComponent" in index_data and "TotalSymbolNo" in index_data:
273 |             actual_count = len(index_data["IndexComponent"])
274 |             if index_data["TotalSymbolNo"] != actual_count:
275 |                 logger.warning(f"TotalSymbolNo ({index_data['TotalSymbolNo']}) doesn't match actual count ({actual_count})")
276 |                 index_data["TotalSymbolNo"] = actual_count
277 |     return response
278 | 
279 | @mcp.tool(
280 |     description="Get list of indices for a specific exchange",
281 | )
282 | async def get_index_list(exchange: str = "hnx", page: int = 1, size: int = 100) -> Dict:
283 |     
284 |     """
285 |     Get list of indices for a specific exchange.
286 |     
287 |     Args:
288 |         exchange (str, optional): Exchange code (hnx, hose). Defaults to "hnx".
289 |         page (int, optional): Page number for pagination. Defaults to 1.
290 |         size (int, optional): Number of records per page. Defaults to 100.
291 |         
292 |     Returns:
293 |         Dict: A dictionary containing indices information with the following structure:
294 |             {
295 |                 "message": str,      # Response message from the API
296 |                 "status": int,       # Status code (200 for success)
297 |                 "totalRecord": int,  # Total number of records available
298 |                 "data": [            # List of indices
299 |                     {
300 |                         "IndexCode": str,    # Index code identifier
301 |                         "IndexName": str,    # Name of the index
302 |                         "Exchange": str,     # Exchange where index is listed (HOSE|HNX)
303 |                     },
304 |                     # ... more indices
305 |                 ]
306 |             }
307 |             
308 |     Raises:
309 |         ValueError: If the exchange is invalid or the response format is invalid
310 |     """
311 |     if not exchange:
312 |         raise ValueError("Exchange code is required")
313 |     
314 |     req = model.index_list(exchange, page, size)
315 |     response = client.index_list(config, req)
316 |     return _process_index_list_response(response)
317 | 
318 | def _process_index_list_response(response: Dict) -> Dict:
319 |     """
320 |     Process and validate the index list API response.
321 |     
322 |     Args:
323 |         response (Dict): The raw response from the API
324 |         
325 |     Returns:
326 |         Dict: Processed response with standardized fields
327 |         
328 |     Raises:
329 |         ValueError: If the response format is invalid
330 |     """
331 |     if response.get("status") != 200:
332 |         logger.warning(f"API returned non-success status: {response.get('status')}")
333 |     if "data" not in response or not isinstance(response["data"], list):
334 |         response["data"] = []
335 |     for index in response["data"]:
336 |         for field in ["IndexCode", "IndexName", "Exchange"]:
337 |             if field not in index:
338 |                 logger.warning(f"Missing field {field} in index data")
339 |                 index[field] = ""
340 |         if "Exchange" in index and index["Exchange"] not in ["HOSE", "HNX"]:
341 |             logger.warning(f"Unexpected Exchange value: {index['Exchange']}")
342 |     
343 |     return response
344 | 
345 | @mcp.tool(
346 |     description="Get daily OHLC data for a specific symbol. Date format: DD/MM/YYYY"
347 | )
348 | async def get_daily_ohlc(symbol: str, from_date: str, to_date: str, 
349 |                         page: int = 1, size: int = 100, ascending: bool = True) -> Dict:
350 |     
351 |     """
352 |     Get daily Open-High-Low-Close (OHLC) data for a specific security symbol.
353 |     
354 |     Args:
355 |         symbol (str): Security symbol/ticker
356 |         from_date (str): Start date in format DD/MM/YYYY
357 |         to_date (str): End date in format DD/MM/YYYY
358 |         page (int, optional): Page number for pagination. Defaults to 1.
359 |         size (int, optional): Number of records per page. Defaults to 100.
360 |         ascending (bool, optional): Sort data in ascending order by date. Defaults to True.
361 |         
362 |     Returns:
363 |         Dict: A dictionary containing OHLC data with the following structure:
364 |             {
365 |                 "message": str,      # Response message from the API
366 |                 "status": int,       # Status code (200 for success)
367 |                 "totalRecord": int,  # Total number of records available
368 |                 "data": [            # List of OHLC data points
369 |                     {
370 |                         "Symbol": str,       # Security symbol/ticker
371 |                         "TradingDate": str,  # Trading date in format dd/mm/yyyy
372 |                         "Time": int,         # Timestamp
373 |                         "Open": float,       # Opening price
374 |                         "High": float,       # Highest price during the day
375 |                         "Low": float,        # Lowest price during the day
376 |                         "Close": float,      # Closing price
377 |                         "Volume": int,       # Total matched volume (normal orders)
378 |                         "Value": float,      # Total matched value (normal orders)
379 |                     },
380 |                     # ... more OHLC data points
381 |                 ]
382 |             }
383 |             
384 |     Raises:
385 |         ValueError: If symbol, from_date, or to_date is not provided.
386 |     """
387 |     _validate_date_params(symbol, from_date, to_date)
388 |     req = model.daily_ohlc(symbol, from_date, to_date, page, size, ascending)
389 |     response = client.daily_ohlc(config, req)
390 |     return _process_ohlc_response(response)
391 | 
392 | def _process_ohlc_response(response: Dict) -> Dict:
393 |     """
394 |     Process and validate the OHLC API response.
395 |     
396 |     Args:
397 |         response (Dict): The raw response from the API
398 |         
399 |     Returns:
400 |         Dict: Processed response with standardized fields
401 |         
402 |     Raises:
403 |         ValueError: If the response format is invalid
404 |     """
405 |     if not isinstance(response, dict):
406 |         raise ValueError("Invalid response format")
407 |     if response.get("status") != 200:
408 |         logger.warning(f"API returned non-success status: {response.get('status')}")
409 |     if "data" not in response or not isinstance(response["data"], list):
410 |         response["data"] = []
411 |     for ohlc_data in response["data"]:
412 |         if "Symbol" not in ohlc_data:
413 |             logger.warning("Missing Symbol field in OHLC data")
414 |             ohlc_data["Symbol"] = ""
415 |             
416 |         for field in ["Open", "High", "Low", "Close", "Volume", "Value"]:
417 |             if field not in ohlc_data:
418 |                 logger.warning(f"Missing {field} field in OHLC data")
419 |                 ohlc_data[field] = 0
420 |             else:
421 |                 try:
422 |                     if isinstance(ohlc_data[field], str):
423 |                         if field == "Volume":
424 |                             ohlc_data[field] = int(ohlc_data[field])
425 |                         else:
426 |                             ohlc_data[field] = float(ohlc_data[field])
427 |                 except (ValueError, TypeError):
428 |                     logger.warning(f"Invalid {field} value: {ohlc_data[field]}")
429 |                     ohlc_data[field] = 0
430 |     
431 |     return response
432 | 
433 | @mcp.tool(
434 |     description="Get intraday OHLC data for a specific symbol. Date format: DD/MM/YYYY"
435 | )
436 | async def get_intraday_ohlc(symbol: str, from_date: str, to_date: str,
437 |                             page: int = 1, size: int = 100, ascending: bool = True, 
438 |                             interval: int = 1) -> Dict:
439 |     
440 |     """
441 |     Get intraday Open-High-Low-Close (OHLC) data for a specific security symbol by tick data.
442 |     
443 |     Args:
444 |         symbol (str): Security symbol/ticker
445 |         from_date (str): Start date in format DD/MM/YYYY
446 |         to_date (str): End date in format DD/MM/YYYY
447 |         page (int, optional): Page number for pagination. Defaults to 1.
448 |         size (int, optional): Number of records per page. Defaults to 100.
449 |         ascending (bool, optional): Sort data in ascending order by time. Defaults to True.
450 |         interval (int, optional): Time interval in minutes. Defaults to 1.
451 |         
452 |     Returns:
453 |         Dict: A dictionary containing intraday OHLC data with the following structure:
454 |             {
455 |                 "message": str,      # Response message from the API
456 |                 "status": int,       # Status code (200 for success)
457 |                 "totalRecord": int,  # Total number of records available
458 |                 "data": [            # List of intraday OHLC data points
459 |                     {
460 |                         "Symbol": str,       # Security symbol/ticker
461 |                         "TradingDate": str,  # Trading date in format dd/mm/yyyy
462 |                         "Time": int,         # Timestamp of the tick data
463 |                         "Open": float,       # Opening price for the interval
464 |                         "High": float,       # Highest price during the interval
465 |                         "Low": float,        # Lowest price during the interval
466 |                         "Close": float,      # Closing price for the interval
467 |                         "Volume": int,       # Total matched volume during the interval
468 |                         "Value": float,      # Total matched value during the interval
469 |                     },
470 |                     # ... more intraday OHLC data points
471 |                 ]
472 |             }
473 |             
474 |     Raises:
475 |         ValueError: If symbol, from_date, or to_date is not provided.
476 |     """
477 |     _validate_date_params(symbol, from_date, to_date)
478 |     req = model.intraday_ohlc(symbol, from_date, to_date, page, size, ascending, interval)
479 |     response = client.intraday_ohlc(config, req)
480 |     return _process_intraday_ohlc_response(response)
481 | 
482 | def _process_intraday_ohlc_response( response: Dict) -> Dict:
483 |     """
484 |     Process and validate the intraday OHLC API response.
485 |     
486 |     Args:
487 |         response (Dict): The raw response from the API
488 |         
489 |     Returns:
490 |         Dict: Processed response with standardized fields
491 |         
492 |     Raises:
493 |         ValueError: If the response format is invalid
494 |     """
495 |     if response.get("status") != 200:
496 |         logger.warning(f"API returned non-success status: {response.get('status')}")
497 |     if "data" not in response or not isinstance(response["data"], list):
498 |         response["data"] = []
499 |     for ohlc_data in response["data"]:
500 |         # Ensure required fields exist with appropriate types
501 |         if "Symbol" not in ohlc_data:
502 |             logger.warning("Missing Symbol field in intraday OHLC data")
503 |             ohlc_data["Symbol"] = ""
504 |         for field in ["Open", "High", "Low", "Close", "Volume", "Value"]:
505 |             if field not in ohlc_data:
506 |                 logger.warning(f"Missing {field} field in intraday OHLC data")
507 |                 ohlc_data[field] = 0
508 |             else:
509 |                 try:
510 |                     if isinstance(ohlc_data[field], str):
511 |                         if field == "Volume":
512 |                             ohlc_data[field] = int(ohlc_data[field])
513 |                         else:
514 |                             ohlc_data[field] = float(ohlc_data[field])
515 |                 except (ValueError, TypeError):
516 |                     logger.warning(f"Invalid {field} value: {ohlc_data[field]}")
517 |                     ohlc_data[field] = 0
518 |         if "Time" not in ohlc_data:
519 |             logger.warning("Missing Time field in intraday OHLC data")
520 |             ohlc_data["Time"] = 0
521 |         if "TradingDate" not in ohlc_data:
522 |             logger.warning("Missing TradingDate field in intraday OHLC data")
523 |             ohlc_data["TradingDate"] = ""
524 |     
525 |     return response
526 | 
527 | @mcp.tool(
528 |     description="Get daily index data( date format: DD/MM/YYYY)"
529 | )
530 | async def get_daily_index( from_date: str, to_date: str, channel_id: str = "123",
531 |                         index: str = "VN100", page: int = 1, size: int = 100) -> Dict:
532 |     
533 |     """
534 |     Get daily trading results of a composite index.
535 |     
536 |     Args:
537 |         from_date (str): Start date in format DD/MM/YYYY
538 |         to_date (str): End date in format DD/MM/YYYY
539 |         channel_id (str, optional): Channel ID. Defaults to "123".
540 |         index (str, optional): Index code. Defaults to "VN100".
541 |         page (int, optional): Page number for pagination. Defaults to 1.
542 |         size (int, optional): Number of records per page. Defaults to 100.
543 |         
544 |     Returns:
545 |         Dict: A dictionary containing daily index data with the following structure:
546 |             {
547 |                 "message": str,      # Response message from the API
548 |                 "status": int,       # Status code (200 for success)
549 |                 "totalRecord": int,  # Total number of records available
550 |                 "data": [            # List of daily index data points
551 |                     {
552 |                         "Indexcode": str,        # Index identifier
553 |                         "IndexValue": float,     # Value of the index
554 |                         "TradingDate": str,      # Trading date in format dd/mm/yyyy
555 |                         "Time": int,             # Timestamp
556 |                         "Change": float,         # Change in index value
557 |                         "RatioChange": float,    # Percentage change
558 |                         "TotalTrade": int,       # Total number of matched orders (both normal and put-through)
559 |                         "Totalmatchvol": int,    # Total matched volume
560 |                         "Totalmatchval": float,  # Total matched value
561 |                         "TypeIndex": str,        # Type of index
562 |                         "IndexName": str,        # Name of the index
563 |                         "Advances": int,         # Total number of advancing stocks
564 |                         "Nochanges": int,        # Total number of unchanged stocks
565 |                         "Declines": int,         # Total number of declining stocks
566 |                         "Ceiling": int,          # Total number of stocks at ceiling price
567 |                         "Floor": int,            # Total number of stocks at floor price
568 |                         "Totaldealvol": int,     # Total volume of put-through orders
569 |                         "Totaldealval": float,   # Total value of put-through orders
570 |                         "Totalvol": int,         # Total volume (both normal and put-through)
571 |                         "Totalval": float,       # Total value (both normal and put-through)
572 |                         "TradingSession": str,   # Trading session
573 |                         "Exchange": str,         # Exchange (HOSE, HNX)
574 |                     },
575 |                     # ... more daily index data points
576 |                 ]
577 |             }
578 |             
579 |     Raises:
580 |         ValueError: If from_date or to_date is not provided.
581 |     """
582 |     if not all([from_date, to_date]):
583 |         raise ValueError("from_date and to_date are required")
584 |     req = model.daily_index(channel_id, index, from_date, to_date, page, size, '', '')
585 |     response = client.daily_index(config, req)
586 |     return _process_daily_index_response(response)
587 | 
588 | def _process_daily_index_response( response: Dict) -> Dict:
589 |     """
590 |     Process and validate the daily index API response.
591 |     
592 |     Args:
593 |         response (Dict): The raw response from the API
594 |         
595 |     Returns:
596 |         Dict: Processed response with standardized fields
597 |         
598 |     Raises:
599 |         ValueError: If the response format is invalid
600 |     """
601 |     if not isinstance(response, dict):
602 |         raise ValueError("Invalid response format")
603 |         
604 |     if response.get("status") != 200:
605 |         logger.warning(f"API returned non-success status: {response.get('status')}")
606 |         
607 |     if "data" not in response or not isinstance(response["data"], list):
608 |         response["data"] = []
609 |     for index_data in response["data"]:
610 |         if "Indexcode" not in index_data:
611 |             logger.warning("Missing Indexcode field in daily index data")
612 |             index_data["Indexcode"] = ""
613 |             
614 |         if "IndexName" not in index_data:
615 |             logger.warning("Missing IndexName field in daily index data")
616 |             index_data["IndexName"] = ""
617 | 
618 |         numeric_fields = [
619 |             "IndexValue", "Change", "RatioChange", "TotalTrade", 
620 |             "Totalmatchvol", "Totalmatchval", "Advances", "Nochanges", 
621 |             "Declines", "Ceiling", "Floor", "Totaldealvol", 
622 |             "Totaldealval", "Totalvol", "Totalval"
623 |         ]
624 |         
625 |         for field in numeric_fields:
626 |             if field not in index_data:
627 |                 logger.warning(f"Missing {field} field in daily index data")
628 |                 index_data[field] = 0
629 |             else:
630 |                 try:
631 |                     if isinstance(index_data[field], str):
632 |                         if field in ["TotalTrade", "Totalmatchvol", "Advances", "Nochanges", 
633 |                                     "Declines", "Ceiling", "Floor", "Totaldealvol", "Totalvol"]:
634 |                             index_data[field] = int(index_data[field])
635 |                         else:
636 |                             index_data[field] = float(index_data[field])
637 |                 except (ValueError, TypeError):
638 |                     logger.warning(f"Invalid {field} value: {index_data[field]}")
639 |                     index_data[field] = 0
640 |         if "TradingDate" not in index_data:
641 |             logger.warning("Missing TradingDate field in daily index data")
642 |             index_data["TradingDate"] = ""
643 |             
644 |         if "Time" not in index_data:
645 |             logger.warning("Missing Time field in daily index data")
646 |             index_data["Time"] = 0
647 |     
648 |     return response
649 | 
650 | @mcp.tool(
651 |     description="Get daily stock price data( include volume, value, foreign buy/sell volume, foreign buy/sell value, total buy/sell volume, total buy/sell value) for a specific symbol. Date format: DD/MM/YYYY"
652 | )
653 | async def get_stock_price(symbol: str, from_date: str, to_date: str,
654 |                         page: int = 1, size: int = 100, exchange: str = "hose") -> Dict:
655 |     
656 |     """
657 |     Get daily stock price data for a specific security symbol.
658 |     
659 |     Args:
660 |         symbol (str): Security symbol/ticker
661 |         from_date (str): Start date in format DD/MM/YYYY
662 |         to_date (str): End date in format DD/MM/YYYY
663 |         page (int, optional): Page number for pagination. Defaults to 1.
664 |         size (int, optional): Number of records per page. Defaults to 100.
665 |         exchange (str, optional): Exchange code (hose, hnx). Defaults to "hose".
666 |         
667 |     Returns:
668 |         Dict: A dictionary containing stock price data with the following structure:
669 |             {
670 |                 "message": str,      # Response message from the API
671 |                 "status": int,       # Status code (200 for success)
672 |                 "totalRecord": int,  # Total number of records available
673 |                 "data": [            # List of stock price data points
674 |                     {
675 |                         "Tradingdate": str,           # Trading date in format dd/mm/yyyy
676 |                         "Symbol": str,                # Security symbol/ticker
677 |                         "Pricechange": str,           # Price change
678 |                         "Perpricechange": str,        # Percentage price change
679 |                         "Ceilingprice": str,          # Ceiling price
680 |                         "Floorprice": str,            # Floor price
681 |                         "Refprice": str,              # Reference price
682 |                         "Openprice": str,             # Opening price
683 |                         "Highestprice": str,          # Highest price
684 |                         "Lowestprice": str,           # Lowest price
685 |                         "Closeprice": str,            # Closing price
686 |                         "Averageprice": str,          # Average price
687 |                         "Closepriceadjusted": str,    # Adjusted closing price
688 |                         "Totalmatchvol": str,         # Total matched volume
689 |                         "Totalmatchval": str,         # Total matched value
690 |                         "Totaldealval": str,          # Total deal value
691 |                         "Totaldealvol": str,          # Total deal volume
692 |                         "Foreignbuyvoltotal": str,    # Total foreign buying volume
693 |                         "Foreigncurrentroom": str,    # Foreign room
694 |                         "Foreignsellvoltotal": str,   # Total foreign selling volume
695 |                         "Foreignbuyvaltotal": str,    # Total foreign buying value
696 |                         "Foreignsellvaltotal": str,   # Total foreign selling value
697 |                         "Totalbuytrade": str,         # Total buy trades
698 |                         "Totalbuytradevol": str,      # Total buy trade volume
699 |                         "Totalselltrade": str,        # Total sell trades
700 |                         "Totalselltradevol": str,     # Total sell trade volume
701 |                         "Netforeivol": str,           # Net foreign volume
702 |                         "Netforeignval": str,         # Net foreign value
703 |                         "Totaltradedvol": str,        # Total traded volume (including matched, put-through, and odd lots)
704 |                         "Totaltradedvalue": str,      # Total traded value (including matched, put-through, and odd lots)
705 |                         "Time": str,                  # Trading time
706 |                     },
707 |                     # ... more stock price data points
708 |                 ]
709 |             }
710 |             
711 |     Raises:
712 |         ValueError: If symbol, from_date, or to_date is not provided.
713 |     """
714 |     _validate_date_params(symbol, from_date, to_date)
715 |     req = model.daily_stock_price(symbol, from_date, to_date, page, size, exchange)
716 |     response = client.daily_stock_price(config, req)
717 |     return _process_stock_price_response(response)
718 | 
719 | def _process_stock_price_response(response: Dict) -> Dict:
720 |     """
721 |     Process and validate the stock price API response.
722 |     
723 |     Args:
724 |         response (Dict): The raw response from the API
725 |         
726 |     Returns:
727 |         Dict: Processed response with standardized fields
728 |         
729 |     Raises:
730 |         ValueError: If the response format is invalid
731 |     """
732 |     if response.get("status") != 200:
733 |         logger.warning(f"API returned non-success status: {response.get('status')}")
734 | 
735 |     if "data" not in response or not isinstance(response["data"], list):
736 |         response["data"] = []
737 |     
738 |     for price_data in response["data"]:
739 |         required_fields = [
740 |             "Symbol", "Tradingdate", "Time", "Pricechange", "Perpricechange",
741 |             "Ceilingprice", "Floorprice", "Refprice", "Openprice", "Highestprice",
742 |             "Lowestprice", "Closeprice", "Averageprice", "Closepriceadjusted",
743 |             "Totalmatchvol", "Totalmatchval", "Totaldealval", "Totaldealvol",
744 |             "Foreignbuyvoltotal", "Foreigncurrentroom", "Foreignsellvoltotal",
745 |             "Foreignbuyvaltotal", "Foreignsellvaltotal", "Totalbuytrade",
746 |             "Totalbuytradevol", "Totalselltrade", "Totalselltradevol",
747 |             "Netforeivol", "Netforeignval", "Totaltradedvol", "Totaltradedvalue"
748 |         ]
749 |         
750 |         for field in required_fields:
751 |             if field not in price_data:
752 |                 logger.warning(f"Missing {field} field in stock price data")
753 |                 price_data[field] = ""
754 |             elif price_data[field] is None:
755 |                 price_data[field] = ""
756 |     
757 |     return response
758 | 
759 | 
760 | dotenv.load_dotenv()
761 | def setup_environment():
762 |     if dotenv.load_dotenv():
763 |         print("Loaded environment variables from .env file")
764 |     else:
765 |         print("No .env file found or could not load it - using environment variables")
766 |     if not config.consumerID:
767 |         print("ERROR: FC_DATA_CONSUMER_ID environment variable is not set")
768 |         print("Please set it to your FC_DATA_CONSUMER_ID")
769 |         return False
770 |     if not config.consumerSecret:
771 |         print("ERROR: FC_DATA_CONSUMER_SECRET environment variable is not set")
772 |         print("Please set it to your FC_DATA_CONSUMER_SECRET")
773 |         return False
774 |     if config.consumerID and config.consumerSecret:
775 |         print("  Authentication: Using secret key")
776 |     return True
777 | 
778 | def run_server():
779 |     """Run the SSI Stock MCP server."""
780 |     if not setup_environment():
781 |         sys.exit(1)
782 |     print("Running server in standard mode...")
783 |     mcp.run(transport="stdio")
784 | 
785 | 
786 | if __name__ == "__main__":
787 |     run_server()
788 | 
```