# 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 | [](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 | 
107 |
108 | 
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 |
```