whitebit-mcp 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- whitebit_mcp-1.0.0/.dockerignore +26 -0
- whitebit_mcp-1.0.0/.env.example +10 -0
- whitebit_mcp-1.0.0/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- whitebit_mcp-1.0.0/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- whitebit_mcp-1.0.0/.github/PULL_REQUEST_TEMPLATE.md +15 -0
- whitebit_mcp-1.0.0/.github/workflows/mirror.yml +84 -0
- whitebit_mcp-1.0.0/.github/workflows/publish-mcp.yml +54 -0
- whitebit_mcp-1.0.0/.gitignore +18 -0
- whitebit_mcp-1.0.0/CONTRIBUTING.md +70 -0
- whitebit_mcp-1.0.0/Dockerfile +21 -0
- whitebit_mcp-1.0.0/LICENSE +202 -0
- whitebit_mcp-1.0.0/PKG-INFO +12 -0
- whitebit_mcp-1.0.0/README.md +302 -0
- whitebit_mcp-1.0.0/SECURITY.md +41 -0
- whitebit_mcp-1.0.0/docker-compose.yml +9 -0
- whitebit_mcp-1.0.0/pyproject.toml +36 -0
- whitebit_mcp-1.0.0/requirements.txt +2 -0
- whitebit_mcp-1.0.0/server.py +352 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
*.pyo
|
|
4
|
+
.venv/
|
|
5
|
+
tests/
|
|
6
|
+
.coverage
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.mypy_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
.vscode/
|
|
11
|
+
.idea/
|
|
12
|
+
.git/
|
|
13
|
+
README.md
|
|
14
|
+
RULES.md
|
|
15
|
+
LESSONS.md
|
|
16
|
+
AGENTS.md
|
|
17
|
+
CLAUDE.md
|
|
18
|
+
AI/
|
|
19
|
+
.env
|
|
20
|
+
docker-compose*.yml
|
|
21
|
+
Dockerfile*
|
|
22
|
+
*.md
|
|
23
|
+
.gitignore
|
|
24
|
+
.github/
|
|
25
|
+
.claude/
|
|
26
|
+
.end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# WhiteBIT MCP Server — environment configuration
|
|
2
|
+
# Copy this file to .env and set values as needed.
|
|
3
|
+
#
|
|
4
|
+
# This is the only server-side setting. API keys are NOT configured here —
|
|
5
|
+
# they are passed directly as parameters in each tool call so the AI client
|
|
6
|
+
# can supply them from conversation context.
|
|
7
|
+
|
|
8
|
+
# WhiteBIT API base URL.
|
|
9
|
+
# Override for testnet or custom deployments.
|
|
10
|
+
WHITEBIT_BASE_URL=https://whitebit.com
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Something isn't working as expected
|
|
4
|
+
labels: bug
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Describe the bug
|
|
8
|
+
|
|
9
|
+
A clear description of what the problem is.
|
|
10
|
+
|
|
11
|
+
## Steps to reproduce
|
|
12
|
+
|
|
13
|
+
1. Start the server with `...`
|
|
14
|
+
2. Send tool call `...`
|
|
15
|
+
3. Observe `...`
|
|
16
|
+
|
|
17
|
+
## Expected behaviour
|
|
18
|
+
|
|
19
|
+
What you expected to happen.
|
|
20
|
+
|
|
21
|
+
## Actual behaviour
|
|
22
|
+
|
|
23
|
+
What actually happened. Include any error messages or stack traces.
|
|
24
|
+
|
|
25
|
+
## Environment
|
|
26
|
+
|
|
27
|
+
- whitebit-mcp version / git commit:
|
|
28
|
+
- `whitebit-python-sdk` version (`pip show whitebit-python-sdk`):
|
|
29
|
+
- Python version:
|
|
30
|
+
- How you're running the server (Docker / local):
|
|
31
|
+
- AI client (Cursor / Claude Code / Claude Desktop):
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature request
|
|
3
|
+
about: Suggest a new tool or improvement
|
|
4
|
+
labels: enhancement
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What would you like?
|
|
8
|
+
|
|
9
|
+
A clear description of the feature or tool you'd like added.
|
|
10
|
+
|
|
11
|
+
## Which SDK client / WhiteBIT API endpoint does this map to?
|
|
12
|
+
|
|
13
|
+
Link to the relevant WhiteBIT API docs if applicable:
|
|
14
|
+
https://docs.whitebit.com/
|
|
15
|
+
|
|
16
|
+
## Why is this useful?
|
|
17
|
+
|
|
18
|
+
Describe the use case this would enable.
|
|
19
|
+
|
|
20
|
+
## Anything else?
|
|
21
|
+
|
|
22
|
+
Additional context, examples, or references.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
<!-- What does this PR do? -->
|
|
4
|
+
|
|
5
|
+
## Changes
|
|
6
|
+
|
|
7
|
+
<!-- List the files changed and why -->
|
|
8
|
+
|
|
9
|
+
## Checklist
|
|
10
|
+
|
|
11
|
+
- [ ] `ruff check .` passes
|
|
12
|
+
- [ ] `ruff format --check .` passes
|
|
13
|
+
- [ ] `mypy --strict server.py` passes with zero errors
|
|
14
|
+
- [ ] If a new SDK client was added: `llms.txt` and `README.md` tool count updated
|
|
15
|
+
- [ ] `CHANGELOG.md` updated under `[Unreleased]`
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
name: Mirror to Public Repo
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, new-mcp, mcp-publishing]
|
|
6
|
+
|
|
7
|
+
env:
|
|
8
|
+
TARGET_REPO: whitebit-exchange/whitebit-mcp
|
|
9
|
+
ROBOT_NAME: WhiteBit Bot
|
|
10
|
+
ROBOT_EMAIL: bot@whitebit.com
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/mcp-publishing'
|
|
15
|
+
uses: ./.github/workflows/publish-mcp.yml
|
|
16
|
+
secrets: inherit
|
|
17
|
+
|
|
18
|
+
mirror:
|
|
19
|
+
needs: publish
|
|
20
|
+
if: always() && (needs.publish.result == 'success' || needs.publish.result == 'skipped')
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- name: Tailscale
|
|
24
|
+
uses: tailscale/github-action@v3
|
|
25
|
+
with:
|
|
26
|
+
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
|
|
27
|
+
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
|
|
28
|
+
tags: tag:ci
|
|
29
|
+
use-cache: 'true'
|
|
30
|
+
|
|
31
|
+
- name: Checkout source
|
|
32
|
+
uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- name: Checkout target repo
|
|
35
|
+
uses: actions/checkout@v4
|
|
36
|
+
with:
|
|
37
|
+
repository: ${{ env.TARGET_REPO }}
|
|
38
|
+
token: ${{ secrets.ROBOT_GITHUB_TOKEN }}
|
|
39
|
+
path: target-repo
|
|
40
|
+
fetch-depth: 0
|
|
41
|
+
persist-credentials: false
|
|
42
|
+
|
|
43
|
+
- name: Sync files
|
|
44
|
+
run: |
|
|
45
|
+
# Clear target (keep .git/)
|
|
46
|
+
find target-repo -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +
|
|
47
|
+
|
|
48
|
+
# Copy files, excluding sensitive and internal ones
|
|
49
|
+
rsync -av \
|
|
50
|
+
--exclude='.git/' \
|
|
51
|
+
--exclude='.github/' \
|
|
52
|
+
--exclude='.env' \
|
|
53
|
+
--exclude='.mcp.json' \
|
|
54
|
+
--exclude='.claude/' \
|
|
55
|
+
--exclude='.idea/' \
|
|
56
|
+
--exclude='.venv/' \
|
|
57
|
+
--exclude='__pycache__/' \
|
|
58
|
+
--exclude='*.pyc' \
|
|
59
|
+
--exclude='.mypy_cache/' \
|
|
60
|
+
--exclude='.ruff_cache/' \
|
|
61
|
+
--exclude='.pytest_cache/' \
|
|
62
|
+
--exclude='.DS_Store' \
|
|
63
|
+
. target-repo/
|
|
64
|
+
|
|
65
|
+
- name: Commit and push
|
|
66
|
+
env:
|
|
67
|
+
ROBOT_GITHUB_TOKEN: ${{ secrets.ROBOT_GITHUB_TOKEN }}
|
|
68
|
+
run: |
|
|
69
|
+
cd target-repo
|
|
70
|
+
|
|
71
|
+
git config user.name "${{ env.ROBOT_NAME }}"
|
|
72
|
+
git config user.email "${{ env.ROBOT_EMAIL }}"
|
|
73
|
+
git add .
|
|
74
|
+
|
|
75
|
+
if ! git diff --quiet --staged; then
|
|
76
|
+
git commit \
|
|
77
|
+
-m "Sync from internal repo" \
|
|
78
|
+
-m "Source: ${{ github.repository }}@${{ github.sha }}"
|
|
79
|
+
else
|
|
80
|
+
echo "No changes to sync"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
git remote set-url origin https://x-access-token:${ROBOT_GITHUB_TOKEN}@github.com/${{ env.TARGET_REPO }}.git
|
|
84
|
+
git push origin HEAD:main
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Publish MCP to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- name: Tailscale
|
|
12
|
+
uses: tailscale/github-action@v3
|
|
13
|
+
with:
|
|
14
|
+
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
|
|
15
|
+
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
|
|
16
|
+
tags: tag:ci
|
|
17
|
+
use-cache: 'true'
|
|
18
|
+
|
|
19
|
+
- name: Checkout
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Setup Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: '3.12'
|
|
26
|
+
|
|
27
|
+
- name: Bump version and publish to PyPI
|
|
28
|
+
env:
|
|
29
|
+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
|
30
|
+
run: |
|
|
31
|
+
CURRENT_VERSION=$(curl -s https://pypi.org/pypi/whitebit-mcp/json | python3 -c "import sys,json; print(json.load(sys.stdin)['info']['version'])" 2>/dev/null || echo "")
|
|
32
|
+
if [ -n "$CURRENT_VERSION" ]; then
|
|
33
|
+
MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1)
|
|
34
|
+
MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2)
|
|
35
|
+
PATCH=$(echo "$CURRENT_VERSION" | cut -d. -f3)
|
|
36
|
+
NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
|
|
37
|
+
else
|
|
38
|
+
NEW_VERSION="1.0.0"
|
|
39
|
+
fi
|
|
40
|
+
echo "New version: $NEW_VERSION"
|
|
41
|
+
|
|
42
|
+
sed -i "s/^version = \".*\"/version = \"$NEW_VERSION\"/" pyproject.toml
|
|
43
|
+
|
|
44
|
+
python -m pip install --upgrade build twine
|
|
45
|
+
python -m build
|
|
46
|
+
twine check dist/*
|
|
47
|
+
TWINE_USERNAME=__token__ TWINE_PASSWORD="${PYPI_TOKEN}" twine upload --repository pypi dist/* --verbose
|
|
48
|
+
|
|
49
|
+
- name: Test installation from PyPI
|
|
50
|
+
run: |
|
|
51
|
+
sleep 30
|
|
52
|
+
pip install whitebit-mcp
|
|
53
|
+
pip show whitebit-mcp | grep "Name: whitebit-mcp"
|
|
54
|
+
echo "Package published and installed successfully"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Contributing to WhiteBit MCP Server
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in improving WhiteBit MCP Server.
|
|
4
|
+
|
|
5
|
+
This server's tools are **auto-generated** from the official [WhiteBit Python SDK](https://github.com/whitebit-exchange/whitebit-python). This means the tool list, parameter names, and descriptions are derived programmatically — changes to individual tools should be addressed upstream in the SDK rather than in this repository.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## How to Contribute
|
|
10
|
+
|
|
11
|
+
### Report a Bug
|
|
12
|
+
|
|
13
|
+
If a tool returns an unexpected result, crashes, or behaves incorrectly:
|
|
14
|
+
|
|
15
|
+
1. Open an [issue](../../issues/new?template=bug_report.md)
|
|
16
|
+
2. Include:
|
|
17
|
+
- The **tool name** (e.g. `spot_trading__create_limit_order`)
|
|
18
|
+
- A **description of the expected vs. actual behavior**
|
|
19
|
+
- The **error message or response** you received (redact credentials)
|
|
20
|
+
- Your **environment**: OS, Docker version or Python version
|
|
21
|
+
- Steps to reproduce
|
|
22
|
+
|
|
23
|
+
### Request a Feature or Tool
|
|
24
|
+
|
|
25
|
+
If you'd like a new tool, endpoint, or behavior:
|
|
26
|
+
|
|
27
|
+
1. Open an [issue](../../issues/new?template=feature_request.md)
|
|
28
|
+
2. Describe:
|
|
29
|
+
- What you want the tool or feature to do
|
|
30
|
+
- Which WhiteBit API endpoint it corresponds to (link to [WhiteBit API docs](https://docs.whitebit.com) if applicable)
|
|
31
|
+
- Your use case
|
|
32
|
+
|
|
33
|
+
> If the missing tool corresponds to a WhiteBit API endpoint that is already present in the Python SDK, it is likely that the tool just needs to be exposed — this can be done quickly.
|
|
34
|
+
|
|
35
|
+
### Report a Documentation Issue
|
|
36
|
+
|
|
37
|
+
If something in the README or docs is wrong, outdated, or unclear:
|
|
38
|
+
|
|
39
|
+
1. Open an [issue](../../issues/new?template=docs.md) describing what needs to be corrected
|
|
40
|
+
2. Include the section and a suggested correction if possible
|
|
41
|
+
|
|
42
|
+
### Ask a Question
|
|
43
|
+
|
|
44
|
+
For usage questions or integration help, open an [issue](../../issues/new) with the `question` label rather than sending email or a direct message. This keeps answers visible to others with the same question.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## What We Do Not Accept
|
|
49
|
+
|
|
50
|
+
Because tool definitions are auto-generated from the SDK, we do **not** accept pull requests that:
|
|
51
|
+
|
|
52
|
+
- Manually add, remove, or rename individual tools
|
|
53
|
+
- Hardcode tool parameters that should come from the SDK
|
|
54
|
+
- Modify generated tool signatures or descriptions
|
|
55
|
+
|
|
56
|
+
If you believe a tool's signature or description is wrong, please report it as a bug — it is likely an issue in the upstream SDK.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Issue Labels
|
|
62
|
+
|
|
63
|
+
| Label | Meaning |
|
|
64
|
+
|-------|---------|
|
|
65
|
+
| `bug` | Something is broken |
|
|
66
|
+
| `feature-request` | New tool or capability |
|
|
67
|
+
| `docs` | Documentation fix or improvement |
|
|
68
|
+
| `question` | Usage or integration question |
|
|
69
|
+
| `upstream` | Root cause is in the WhiteBit SDK |
|
|
70
|
+
| `wontfix` | Out of scope for this repository |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
FROM python:3.13-slim AS builder
|
|
2
|
+
|
|
3
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/
|
|
4
|
+
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
COPY pyproject.toml .
|
|
8
|
+
RUN uv sync --no-install-project --no-dev
|
|
9
|
+
|
|
10
|
+
FROM python:3.13-slim
|
|
11
|
+
|
|
12
|
+
COPY --from=builder /app/.venv /app/.venv
|
|
13
|
+
ENV PATH="/app/.venv/bin:$PATH"
|
|
14
|
+
ENV PYTHONUNBUFFERED=1
|
|
15
|
+
|
|
16
|
+
WORKDIR /app
|
|
17
|
+
COPY server.py .
|
|
18
|
+
|
|
19
|
+
EXPOSE 8000
|
|
20
|
+
|
|
21
|
+
ENTRYPOINT ["python", "server.py"]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whitebit-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP server that exposes the WhiteBIT exchange API as AI tools
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
9
|
+
Requires-Dist: whitebit-python-sdk>=1.1.6
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
<h1 align="center">WhiteBit MCP Server</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>Connect AI assistants to WhiteBit — trade, query, and manage your crypto portfolio through natural language.</strong>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://img.shields.io/badge/Python-3.11+-3776AB?style=flat-square&logo=python" alt="Python 3.11+" />
|
|
9
|
+
<img src="https://img.shields.io/badge/MCP-compatible-8A2BE2?style=flat-square" alt="MCP compatible" />
|
|
10
|
+
<img src="https://img.shields.io/badge/transport-HTTP-0070f3?style=flat-square" alt="HTTP transport" />
|
|
11
|
+
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="MIT license" />
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
**WhiteBit MCP Server** is a [Model Context Protocol](https://modelcontextprotocol.io) server for the [WhiteBit](https://whitebit.com) cryptocurrency exchange. It exposes 100+ trading and account tools — auto-generated from the official WhiteBit Python SDK — that any MCP-compatible AI assistant can call through natural language. Check prices, manage orders, query balances, handle withdrawals, and more.
|
|
17
|
+
|
|
18
|
+
Works with **Claude Code**, **Claude Desktop**, **Cursor**, and any other MCP-compatible client.
|
|
19
|
+
|
|
20
|
+
> **Credentials are passed as tool parameters** (`api_key`, `secret_key`), not as server-level configuration. This means you can use different WhiteBit accounts within the same session without restarting the server.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
Before you start, make sure you have the following:
|
|
27
|
+
|
|
28
|
+
| Requirement | Details |
|
|
29
|
+
|-------------|---------|
|
|
30
|
+
| **Docker & Docker Compose** | To run the server — [install Docker](https://docs.docker.com/get-docker/) |
|
|
31
|
+
| **WhiteBit account** | Sign up at [whitebit.com](https://whitebit.com) |
|
|
32
|
+
| **WhiteBit API key** | Profile → API keys → Create key (Read and/or Trade permissions) |
|
|
33
|
+
| **MCP-compatible AI client** | Claude Code, Claude Desktop, Cursor, or any other MCP client |
|
|
34
|
+
| **Python 3.11+** | Only if running without Docker |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
### 1. Get your WhiteBit API credentials
|
|
41
|
+
|
|
42
|
+
1. Log in to [whitebit.com](https://whitebit.com) → **Profile → API keys**
|
|
43
|
+
2. Create a new key — choose **Read** and/or **Trade** permissions as needed
|
|
44
|
+
3. Copy your **API Key** and **Secret Key**
|
|
45
|
+
|
|
46
|
+
> Public endpoints (market data, tickers, order book) work without credentials. Private endpoints (account, trading) require both.
|
|
47
|
+
|
|
48
|
+
### 2. Start the server
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/your-org/whitebit-mcp.git
|
|
52
|
+
cd whitebit-mcp
|
|
53
|
+
docker compose up -d
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The server starts at `http://localhost:8000`.
|
|
57
|
+
|
|
58
|
+
### 3. Add to your AI client
|
|
59
|
+
|
|
60
|
+
#### Claude Code (project-level)
|
|
61
|
+
|
|
62
|
+
Create or update `.mcp.json` in your project root:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"whitebit-mcp": {
|
|
68
|
+
"type": "http",
|
|
69
|
+
"url": "http://localhost:8000/mcp"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or via CLI:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
claude mcp add whitebit-mcp "http://localhost:8000/mcp" -t http -s user
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> Credentials (`api_key`, `secret_key`) are passed directly to each tool call — the server does not store them.
|
|
82
|
+
|
|
83
|
+
That's it. Your AI can now trade on WhiteBit.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Integrations
|
|
88
|
+
|
|
89
|
+
### VS Code (Claude Extension)
|
|
90
|
+
|
|
91
|
+
Install the [Claude extension for VS Code](https://marketplace.visualstudio.com/items?itemName=Anthropic.claude-code), then add the server via CLI:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
claude mcp add whitebit-mcp "http://localhost:8000/mcp" -t http -s user
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Or create `.mcp.json` in your project root to share the config with your team:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mcpServers": {
|
|
102
|
+
"whitebit-mcp": {
|
|
103
|
+
"type": "http",
|
|
104
|
+
"url": "http://localhost:8000/mcp"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Once added, use `/mcp` in the Claude chat panel to enable, disable, or reconnect the server.
|
|
111
|
+
|
|
112
|
+
> `.mcp.json` is in `.gitignore` by default — if you want to share it with your team, remove it from `.gitignore` first.
|
|
113
|
+
|
|
114
|
+
### Claude Desktop
|
|
115
|
+
|
|
116
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"mcpServers": {
|
|
121
|
+
"whitebit-mcp": {
|
|
122
|
+
"type": "http",
|
|
123
|
+
"url": "http://localhost:8000/mcp"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Cursor
|
|
130
|
+
|
|
131
|
+
Add to your Cursor MCP settings (`~/.cursor/mcp.json`):
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"mcpServers": {
|
|
136
|
+
"whitebit-mcp": {
|
|
137
|
+
"type": "http",
|
|
138
|
+
"url": "http://localhost:8000/mcp"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Codex
|
|
145
|
+
|
|
146
|
+
Add to your Codex MCP settings (`~/.codex/config.toml`):
|
|
147
|
+
|
|
148
|
+
```toml
|
|
149
|
+
[[mcp_servers]]
|
|
150
|
+
name = "whitebit-mcp"
|
|
151
|
+
type = "http"
|
|
152
|
+
url = "http://localhost:8000/mcp"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Or using the JSON format (`~/.codex/mcp.json`):
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"mcpServers": {
|
|
160
|
+
"whitebit-mcp": {
|
|
161
|
+
"type": "http",
|
|
162
|
+
"url": "http://localhost:8000/mcp"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### OpenClaw
|
|
169
|
+
|
|
170
|
+
Add via CLI:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
openclaw mcp set whitebit-mcp '{"url":"http://localhost:8000/mcp"}'
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Or add to your OpenClaw config under `mcp.servers`:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"mcp": {
|
|
181
|
+
"servers": {
|
|
182
|
+
"whitebit-mcp": {
|
|
183
|
+
"url": "http://localhost:8000/mcp"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Any MCP-compatible client
|
|
191
|
+
|
|
192
|
+
The server uses standard **Streamable HTTP transport** on `http://localhost:8000/mcp`. No server-level authentication is required — credentials are supplied per tool call.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Usage Examples
|
|
197
|
+
|
|
198
|
+
Once connected, talk to your AI naturally. It will prompt you for credentials when needed:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
"What's the current BTC/USDT price?"
|
|
202
|
+
"Show me my spot account balance"
|
|
203
|
+
"Place a limit buy order for 0.01 BTC at $95,000"
|
|
204
|
+
"Cancel all my open orders on ETH/USDT"
|
|
205
|
+
"What are the trading fees for BTC/USDT?"
|
|
206
|
+
"Transfer 100 USDT from my main account to my trade account"
|
|
207
|
+
"Show my open collateral positions"
|
|
208
|
+
"Withdraw 500 USDT to address 0x..."
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## How Credentials Work
|
|
214
|
+
|
|
215
|
+
Unlike header-based servers, this server receives `api_key` and `secret_key` as explicit parameters on every tool call. The AI assistant supplies them from the conversation context.
|
|
216
|
+
|
|
217
|
+
| Endpoint type | Required parameters |
|
|
218
|
+
|---------------|---------------------|
|
|
219
|
+
| Public (market data) | `api_key`, `secret_key` |
|
|
220
|
+
| Private (trading, account) | `api_key`, `secret_key` |
|
|
221
|
+
| Account endpoints (OAuth2) | `api_key`, `bearer_token` |
|
|
222
|
+
|
|
223
|
+
To obtain a `bearer_token` for account endpoints, use the `authentication__get_access_token` tool first.
|
|
224
|
+
|
|
225
|
+
Use `get_credentials_status` to verify that credentials are being passed correctly.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Configuration
|
|
230
|
+
|
|
231
|
+
Copy `.env.example` to `.env` and adjust as needed:
|
|
232
|
+
|
|
233
|
+
```env
|
|
234
|
+
# WhiteBit API base URL
|
|
235
|
+
WHITEBIT_BASE_URL=https://whitebit.com
|
|
236
|
+
|
|
237
|
+
# Server port (default: 8000)
|
|
238
|
+
PORT=8000
|
|
239
|
+
|
|
240
|
+
# Log level: debug | info | warn | error
|
|
241
|
+
LOG_LEVEL=info
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
## Available Tools
|
|
246
|
+
|
|
247
|
+
100+ tools auto-generated from the official WhiteBit Python SDK across 19 categories:
|
|
248
|
+
|
|
249
|
+
| Category | Tool prefix |
|
|
250
|
+
|----------|-------------|
|
|
251
|
+
| **Authentication** | `authentication__` |
|
|
252
|
+
| **Account endpoints** | `account_endpoints__` |
|
|
253
|
+
| **Public API v4** | `public_api_v4__` |
|
|
254
|
+
| **Main account** | `main_account__` |
|
|
255
|
+
| **Deposit** | `deposit__` |
|
|
256
|
+
| **Withdraw** | `withdraw__` |
|
|
257
|
+
| **Transfer** | `transfer__` |
|
|
258
|
+
| **Codes** | `codes__` |
|
|
259
|
+
| **Spot trading** | `spot_trading__` |
|
|
260
|
+
| **Collateral trading** | `collateral_trading__` |
|
|
261
|
+
| **Market fee** | `market_fee__` |
|
|
262
|
+
| **Fees** | `fees__` |
|
|
263
|
+
| **Convert** | `convert_estimate`, `convert_confirm`, `convert_history` |
|
|
264
|
+
| **Crypto lending (flex)** | `crypto_lending_flex__` |
|
|
265
|
+
| **Crypto lending (fixed)** | `crypto_lending_fixed__` |
|
|
266
|
+
| **Sub-account** | `sub_account__` |
|
|
267
|
+
| **Sub-account API keys** | `sub_account_api_keys__` |
|
|
268
|
+
| **Mining pool** | `mining_pool__` |
|
|
269
|
+
| **Credit line** | `credit_line__` |
|
|
270
|
+
| **Credentials** | `get_credentials_status` |
|
|
271
|
+
|
|
272
|
+
Tools are named `{category}__{method}` (e.g. `spot_trading__create_limit_order`). All tool names and parameters are derived directly from the SDK — no manual mapping.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Security
|
|
277
|
+
|
|
278
|
+
- Credentials are passed per tool call — **never stored** in server memory or logs
|
|
279
|
+
- Use **read-only API keys** if you only need market data or account queries
|
|
280
|
+
- For trading, create a dedicated API key with only the permissions you need
|
|
281
|
+
- Consider IP whitelisting on your WhiteBit API key for additional protection
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Running without Docker
|
|
286
|
+
|
|
287
|
+
Requirements: **Python 3.11+**
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
pip install -r requirements.txt
|
|
291
|
+
|
|
292
|
+
# Run
|
|
293
|
+
python server.py
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The server listens on `PORT` (default `8000`).
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
Please do **not** open a public GitHub issue for security vulnerabilities.
|
|
6
|
+
|
|
7
|
+
Report them privately via [GitHub Security Advisories](https://github.com/whitebit-exchange/whitebit-mcp/security/advisories/new).
|
|
8
|
+
Include a description of the issue, steps to reproduce, and potential impact.
|
|
9
|
+
We will respond within 5 business days.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Credential Model
|
|
14
|
+
|
|
15
|
+
API keys are passed as parameters in each individual tool call. The server:
|
|
16
|
+
|
|
17
|
+
- Uses them only to sign the outgoing WhiteBIT API request for that call
|
|
18
|
+
- Does not store, log, cache, or persist them in any form
|
|
19
|
+
- Does not write them to disk or transmit them to any party other than the WhiteBIT API
|
|
20
|
+
|
|
21
|
+
Keys exist in memory only for the duration of a single HTTP request.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Recommendations
|
|
26
|
+
|
|
27
|
+
**Use minimal-permission API keys.** WhiteBIT lets you restrict each key to specific
|
|
28
|
+
operations. Examples:
|
|
29
|
+
|
|
30
|
+
| Use case | Permissions needed |
|
|
31
|
+
|---|---|
|
|
32
|
+
| Market data only | Any non-empty string (public endpoints do not validate keys) |
|
|
33
|
+
| Balance and order queries | Read balance, Read orders |
|
|
34
|
+
| Spot order placement | Read balance, Spot trading |
|
|
35
|
+
| Full access | All permissions |
|
|
36
|
+
|
|
37
|
+
**Keep keys out of your repository.** Do not commit API keys to `.env`, `docker-compose.yml`,
|
|
38
|
+
or any configuration file. Pass them in conversation when using an AI client.
|
|
39
|
+
|
|
40
|
+
**Use sub-accounts for automated strategies.** Create a dedicated sub-account with scoped API
|
|
41
|
+
keys rather than using your main account keys.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "whitebit-mcp"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "MCP server that exposes the WhiteBIT exchange API as AI tools"
|
|
9
|
+
license = { text = "MIT" }
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"whitebit-python-sdk>=1.1.6",
|
|
13
|
+
"mcp[cli]>=1.0.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"ruff>=0.4.0",
|
|
19
|
+
"mypy>=1.0.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
# server.py is a standalone script, not a library package.
|
|
24
|
+
# This entry enables `pip install -e ".[dev]"` for local dev tooling only.
|
|
25
|
+
include = ["server.py"]
|
|
26
|
+
|
|
27
|
+
[tool.ruff]
|
|
28
|
+
line-length = 100
|
|
29
|
+
target-version = "py312"
|
|
30
|
+
|
|
31
|
+
[tool.ruff.lint]
|
|
32
|
+
select = ["E", "F", "I", "N", "UP", "B", "A", "C4", "RUF"]
|
|
33
|
+
|
|
34
|
+
[tool.mypy]
|
|
35
|
+
python_version = "3.12"
|
|
36
|
+
strict = true
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import hmac as _hmac
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from mcp.server.fastmcp import FastMCP
|
|
11
|
+
|
|
12
|
+
from whitebit.core.client_wrapper import AsyncClientWrapper
|
|
13
|
+
from whitebit.environment import WhitebitApiEnvironment
|
|
14
|
+
from whitebit.authentication.client import AsyncAuthenticationClient
|
|
15
|
+
from whitebit.account_endpoints.client import AsyncAccountEndpointsClient
|
|
16
|
+
from whitebit.public_api_v4.client import AsyncPublicApiV4Client
|
|
17
|
+
from whitebit.main_account.client import AsyncMainAccountClient
|
|
18
|
+
from whitebit.deposit.client import AsyncDepositClient
|
|
19
|
+
from whitebit.jwt.client import AsyncJwtClient
|
|
20
|
+
from whitebit.withdraw.client import AsyncWithdrawClient
|
|
21
|
+
from whitebit.transfer.client import AsyncTransferClient
|
|
22
|
+
from whitebit.codes.client import AsyncCodesClient
|
|
23
|
+
from whitebit.crypto_lending_fixed.client import AsyncCryptoLendingFixedClient
|
|
24
|
+
from whitebit.crypto_lending_flex.client import AsyncCryptoLendingFlexClient
|
|
25
|
+
from whitebit.fees.client import AsyncFeesClient
|
|
26
|
+
from whitebit.sub_account.client import AsyncSubAccountClient
|
|
27
|
+
from whitebit.sub_account_api_keys.client import AsyncSubAccountApiKeysClient
|
|
28
|
+
from whitebit.mining_pool.client import AsyncMiningPoolClient
|
|
29
|
+
from whitebit.credit_line.client import AsyncCreditLineClient
|
|
30
|
+
from whitebit.collateral_trading.client import AsyncCollateralTradingClient
|
|
31
|
+
from whitebit.market_fee.client import AsyncMarketFeeClient
|
|
32
|
+
from whitebit.spot_trading.client import AsyncSpotTradingClient
|
|
33
|
+
from whitebit.client import AsyncWhitebitApi, OMIT
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Credentials are passed as tool parameters (api_key, secret_key) so that
|
|
37
|
+
# the LLM can supply them from conversation context.
|
|
38
|
+
# WHITEBIT_BASE_URL remains an env-var-only config (useful for tests).
|
|
39
|
+
# account_endpoints use OAuth2 Bearer auth; supply bearer_token to use them.
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
SUBCLIENT_CLASSES: dict[str, type] = {
|
|
43
|
+
"authentication": AsyncAuthenticationClient,
|
|
44
|
+
"account_endpoints": AsyncAccountEndpointsClient,
|
|
45
|
+
"public_api_v4": AsyncPublicApiV4Client,
|
|
46
|
+
"main_account": AsyncMainAccountClient,
|
|
47
|
+
"deposit": AsyncDepositClient,
|
|
48
|
+
"jwt": AsyncJwtClient,
|
|
49
|
+
"withdraw": AsyncWithdrawClient,
|
|
50
|
+
"transfer": AsyncTransferClient,
|
|
51
|
+
"codes": AsyncCodesClient,
|
|
52
|
+
"crypto_lending_fixed": AsyncCryptoLendingFixedClient,
|
|
53
|
+
"crypto_lending_flex": AsyncCryptoLendingFlexClient,
|
|
54
|
+
"fees": AsyncFeesClient,
|
|
55
|
+
"sub_account": AsyncSubAccountClient,
|
|
56
|
+
"sub_account_api_keys": AsyncSubAccountApiKeysClient,
|
|
57
|
+
"mining_pool": AsyncMiningPoolClient,
|
|
58
|
+
"credit_line": AsyncCreditLineClient,
|
|
59
|
+
"collateral_trading": AsyncCollateralTradingClient,
|
|
60
|
+
"market_fee": AsyncMarketFeeClient,
|
|
61
|
+
"spot_trading": AsyncSpotTradingClient,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_TOP_LEVEL_METHODS = ("convert_estimate", "convert_confirm", "convert_history")
|
|
65
|
+
|
|
66
|
+
# Credential parameters injected into every tool signature.
|
|
67
|
+
_CRED_PARAMS = [
|
|
68
|
+
inspect.Parameter("api_key", kind=inspect.Parameter.KEYWORD_ONLY, annotation=str),
|
|
69
|
+
inspect.Parameter("secret_key", kind=inspect.Parameter.KEYWORD_ONLY, annotation=str),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# secret_key as optional — for endpoints that don't use HMAC signing.
|
|
73
|
+
_SECRET_KEY_OPTIONAL = inspect.Parameter(
|
|
74
|
+
"secret_key",
|
|
75
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
76
|
+
annotation=str,
|
|
77
|
+
default="",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Extra bearer_token parameter injected into account_endpoints tools.
|
|
81
|
+
_BEARER_PARAM = inspect.Parameter(
|
|
82
|
+
"bearer_token",
|
|
83
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
84
|
+
annotation=str,
|
|
85
|
+
default="",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# HMAC signing fields injected by the transport — stripped from SDK method params.
|
|
89
|
+
_HMAC_FIELDS = {"request", "nonce"}
|
|
90
|
+
|
|
91
|
+
# SDK sends these snake_case keys but WhiteBit API expects camelCase.
|
|
92
|
+
_SNAKE_TO_CAMEL = {
|
|
93
|
+
"order_id": "orderId",
|
|
94
|
+
"client_order_id": "clientOrderId",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_ENV = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get_environment() -> WhitebitApiEnvironment:
|
|
101
|
+
global _ENV
|
|
102
|
+
if _ENV is None:
|
|
103
|
+
base_url = os.environ.get("WHITEBIT_BASE_URL", "https://whitebit.com")
|
|
104
|
+
_ENV = WhitebitApiEnvironment(
|
|
105
|
+
base=base_url,
|
|
106
|
+
production=WhitebitApiEnvironment.DEFAULT.production,
|
|
107
|
+
eu=WhitebitApiEnvironment.DEFAULT.eu,
|
|
108
|
+
)
|
|
109
|
+
return _ENV
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _is_credentials_error(exc: Exception) -> bool:
|
|
113
|
+
msg = str(exc).lower()
|
|
114
|
+
return "invalid payload" in msg or "code: 9" in msg
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class WhitebitHmacTransport(httpx.AsyncBaseTransport):
|
|
118
|
+
"""Signs POST requests with WhiteBit HMAC-SHA512."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, api_key: str, secret_key: str):
|
|
121
|
+
self._api_key = api_key
|
|
122
|
+
self._secret_key = secret_key
|
|
123
|
+
self._inner = httpx.AsyncHTTPTransport()
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _ua_for(request: httpx.Request) -> bytes:
|
|
127
|
+
existing = request.headers.get("user-agent", "")
|
|
128
|
+
return (f"{existing} mcp/python" if existing else "mcp/python").encode()
|
|
129
|
+
|
|
130
|
+
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
|
131
|
+
if request.method == "POST":
|
|
132
|
+
try:
|
|
133
|
+
body_bytes = request.content
|
|
134
|
+
except httpx.RequestNotRead:
|
|
135
|
+
body_bytes = b""
|
|
136
|
+
|
|
137
|
+
body_dict = {}
|
|
138
|
+
if body_bytes:
|
|
139
|
+
try:
|
|
140
|
+
body_dict = json.loads(body_bytes)
|
|
141
|
+
except (json.JSONDecodeError, ValueError):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# Rename snake_case keys that WhiteBit API expects as camelCase.
|
|
145
|
+
for snake, camel in _SNAKE_TO_CAMEL.items():
|
|
146
|
+
if snake in body_dict:
|
|
147
|
+
body_dict[camel] = body_dict.pop(snake)
|
|
148
|
+
|
|
149
|
+
body_dict["request"] = request.url.path
|
|
150
|
+
body_dict["nonce"] = time.time_ns()
|
|
151
|
+
|
|
152
|
+
new_body = json.dumps(body_dict, separators=(',', ':')).encode()
|
|
153
|
+
payload = base64.b64encode(new_body).decode()
|
|
154
|
+
signature = _hmac.new(
|
|
155
|
+
self._secret_key.encode(), payload.encode(), hashlib.sha512
|
|
156
|
+
).hexdigest()
|
|
157
|
+
|
|
158
|
+
new_headers = [
|
|
159
|
+
(k, v) for k, v in request.headers.raw
|
|
160
|
+
if k.lower() not in (
|
|
161
|
+
b"authorization", b"content-type", b"content-length",
|
|
162
|
+
b"x-txc-apikey", b"x-txc-payload", b"x-txc-signature",
|
|
163
|
+
b"user-agent",
|
|
164
|
+
)
|
|
165
|
+
]
|
|
166
|
+
new_headers += [
|
|
167
|
+
(b"x-txc-apikey", self._api_key.encode()),
|
|
168
|
+
(b"x-txc-payload", payload.encode()),
|
|
169
|
+
(b"x-txc-signature", signature.encode()),
|
|
170
|
+
(b"content-type", b"application/json"),
|
|
171
|
+
(b"content-length", str(len(new_body)).encode()),
|
|
172
|
+
(b"user-agent", self._ua_for(request)),
|
|
173
|
+
]
|
|
174
|
+
request = httpx.Request(
|
|
175
|
+
method=request.method,
|
|
176
|
+
url=request.url,
|
|
177
|
+
headers=new_headers,
|
|
178
|
+
content=new_body,
|
|
179
|
+
extensions=request.extensions,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
else:
|
|
183
|
+
new_headers = [(k, v) for k, v in request.headers.raw if k.lower() != b"user-agent"]
|
|
184
|
+
new_headers.append((b"user-agent", self._ua_for(request)))
|
|
185
|
+
try:
|
|
186
|
+
body = request.content
|
|
187
|
+
except httpx.RequestNotRead:
|
|
188
|
+
body = b""
|
|
189
|
+
request = httpx.Request(
|
|
190
|
+
method=request.method,
|
|
191
|
+
url=request.url,
|
|
192
|
+
headers=new_headers,
|
|
193
|
+
content=body,
|
|
194
|
+
extensions=request.extensions,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return await self._inner.handle_async_request(request)
|
|
198
|
+
|
|
199
|
+
async def aclose(self):
|
|
200
|
+
await self._inner.aclose()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class _NoAuthClientWrapper(AsyncClientWrapper):
|
|
204
|
+
"""AsyncClientWrapper that omits the Authorization header (for OAuth2 exchange endpoints)."""
|
|
205
|
+
|
|
206
|
+
def get_headers(self) -> dict:
|
|
207
|
+
return {"X-Fern-Language": "Python", "X-TXC-APIKEY": self._txc_apikey}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
mcp = FastMCP("whitebit-mcp", host="0.0.0.0", port=8000)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _make_tool(subclient_attr: str | None, method_name: str, original_sig: inspect.Signature):
|
|
214
|
+
is_account_endpoint = subclient_attr == "account_endpoints"
|
|
215
|
+
is_authentication = subclient_attr == "authentication"
|
|
216
|
+
|
|
217
|
+
orig_params = [
|
|
218
|
+
p.replace(kind=inspect.Parameter.KEYWORD_ONLY, default=None)
|
|
219
|
+
if p.default is OMIT
|
|
220
|
+
else p.replace(kind=inspect.Parameter.KEYWORD_ONLY)
|
|
221
|
+
for name, p in original_sig.parameters.items()
|
|
222
|
+
if name not in ("self", "request_options") and name not in _HMAC_FIELDS
|
|
223
|
+
]
|
|
224
|
+
# Remember which HMAC fields the method needs so we can inject dummies.
|
|
225
|
+
needs_hmac_fields = {
|
|
226
|
+
name for name in original_sig.parameters
|
|
227
|
+
if name in _HMAC_FIELDS
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# HMAC endpoints need api_key + secret_key.
|
|
231
|
+
# authentication only needs api_key; account_endpoints need api_key + bearer_token.
|
|
232
|
+
_api_key_param = _CRED_PARAMS[0]
|
|
233
|
+
if is_account_endpoint:
|
|
234
|
+
new_sig = original_sig.replace(parameters=[_api_key_param, _SECRET_KEY_OPTIONAL, _BEARER_PARAM] + orig_params)
|
|
235
|
+
elif is_authentication:
|
|
236
|
+
new_sig = original_sig.replace(parameters=[_api_key_param, _SECRET_KEY_OPTIONAL] + orig_params)
|
|
237
|
+
else:
|
|
238
|
+
new_sig = original_sig.replace(parameters=_CRED_PARAMS + orig_params)
|
|
239
|
+
|
|
240
|
+
async def tool(**kwargs):
|
|
241
|
+
api_key = kwargs.pop("api_key")
|
|
242
|
+
secret_key = kwargs.pop("secret_key", "")
|
|
243
|
+
bearer_token = kwargs.pop("bearer_token", "")
|
|
244
|
+
|
|
245
|
+
needs_hmac = not is_authentication and not is_account_endpoint
|
|
246
|
+
if not api_key or (needs_hmac and not secret_key):
|
|
247
|
+
raise RuntimeError(
|
|
248
|
+
"❌ api_key and secret_key must be provided as tool parameters."
|
|
249
|
+
if needs_hmac else
|
|
250
|
+
"❌ api_key must be provided as tool parameter."
|
|
251
|
+
)
|
|
252
|
+
if is_account_endpoint and not bearer_token:
|
|
253
|
+
raise RuntimeError(
|
|
254
|
+
"❌ account_endpoints require a bearer_token (OAuth2 access token). "
|
|
255
|
+
"Obtain one via authentication__get_access_token first."
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
cleaned = {k: v for k, v in kwargs.items() if v is not None}
|
|
259
|
+
# Inject dummy values for HMAC signing fields — transport overwrites them.
|
|
260
|
+
for field in needs_hmac_fields:
|
|
261
|
+
cleaned.setdefault(field, "auto")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
if subclient_attr is None:
|
|
265
|
+
transport = WhitebitHmacTransport(api_key=api_key, secret_key=secret_key)
|
|
266
|
+
async with httpx.AsyncClient(transport=transport) as hmac_client:
|
|
267
|
+
obj = AsyncWhitebitApi(
|
|
268
|
+
txc_apikey=api_key, token="unused",
|
|
269
|
+
environment=_get_environment(), httpx_client=hmac_client,
|
|
270
|
+
)
|
|
271
|
+
return await getattr(obj, method_name)(**cleaned)
|
|
272
|
+
elif is_account_endpoint:
|
|
273
|
+
async with httpx.AsyncClient() as plain_client:
|
|
274
|
+
wrapper = AsyncClientWrapper(
|
|
275
|
+
txc_apikey=api_key, token=bearer_token,
|
|
276
|
+
environment=_get_environment(), httpx_client=plain_client,
|
|
277
|
+
)
|
|
278
|
+
obj = AsyncAccountEndpointsClient(client_wrapper=wrapper)
|
|
279
|
+
return await getattr(obj, method_name)(**cleaned)
|
|
280
|
+
elif is_authentication:
|
|
281
|
+
async with httpx.AsyncClient() as plain_client:
|
|
282
|
+
wrapper = _NoAuthClientWrapper(
|
|
283
|
+
txc_apikey=api_key, token="unused",
|
|
284
|
+
environment=_get_environment(), httpx_client=plain_client,
|
|
285
|
+
)
|
|
286
|
+
obj = AsyncAuthenticationClient(client_wrapper=wrapper)
|
|
287
|
+
return await getattr(obj, method_name)(**cleaned)
|
|
288
|
+
else:
|
|
289
|
+
transport = WhitebitHmacTransport(api_key=api_key, secret_key=secret_key)
|
|
290
|
+
async with httpx.AsyncClient(transport=transport) as hmac_client:
|
|
291
|
+
wrapper = AsyncClientWrapper(
|
|
292
|
+
txc_apikey=api_key, token="unused",
|
|
293
|
+
environment=_get_environment(), httpx_client=hmac_client,
|
|
294
|
+
)
|
|
295
|
+
subclient_cls = SUBCLIENT_CLASSES[subclient_attr]
|
|
296
|
+
obj = subclient_cls(client_wrapper=wrapper)
|
|
297
|
+
return await getattr(obj, method_name)(**cleaned)
|
|
298
|
+
except RuntimeError:
|
|
299
|
+
raise
|
|
300
|
+
except Exception as exc:
|
|
301
|
+
if _is_credentials_error(exc):
|
|
302
|
+
raise RuntimeError(
|
|
303
|
+
f"❌ WhiteBit API auth/request error: {exc}"
|
|
304
|
+
) from None
|
|
305
|
+
raise
|
|
306
|
+
|
|
307
|
+
tool.__name__ = method_name
|
|
308
|
+
tool.__signature__ = new_sig
|
|
309
|
+
return tool
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def register_whitebit_tools():
|
|
313
|
+
for name in _TOP_LEVEL_METHODS:
|
|
314
|
+
method = getattr(AsyncWhitebitApi, name)
|
|
315
|
+
fn = _make_tool(None, name, inspect.signature(method))
|
|
316
|
+
fn.__doc__ = method.__doc__
|
|
317
|
+
mcp.tool(name=name, description=(method.__doc__ or "").strip().split("\n")[0])(fn)
|
|
318
|
+
|
|
319
|
+
for attr_name, subclient_cls in SUBCLIENT_CLASSES.items():
|
|
320
|
+
for method_name, method in inspect.getmembers(subclient_cls, predicate=inspect.isfunction):
|
|
321
|
+
if method_name.startswith("_"):
|
|
322
|
+
continue
|
|
323
|
+
tool_name = f"{attr_name}__{method_name}"
|
|
324
|
+
fn = _make_tool(attr_name, method_name, inspect.signature(method))
|
|
325
|
+
fn.__doc__ = method.__doc__
|
|
326
|
+
description = (method.__doc__ or "").strip().split("\n")[0]
|
|
327
|
+
mcp.tool(name=tool_name, description=description)(fn)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
register_whitebit_tools()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@mcp.tool(
|
|
334
|
+
name="get_credentials_status",
|
|
335
|
+
description="Check whether WhiteBit API credentials are valid by echoing masked values.",
|
|
336
|
+
)
|
|
337
|
+
async def get_credentials_status(api_key: str, secret_key: str) -> str:
|
|
338
|
+
base_url = os.environ.get("WHITEBIT_BASE_URL", "https://whitebit.com")
|
|
339
|
+
if api_key and secret_key:
|
|
340
|
+
masked_key = api_key[:4] + "****" + api_key[-4:] if len(api_key) > 8 else "****"
|
|
341
|
+
masked_secret = "****" + secret_key[-4:] if len(secret_key) > 4 else "****"
|
|
342
|
+
return (
|
|
343
|
+
f"✅ Credentials provided.\n"
|
|
344
|
+
f" API key: {masked_key}\n"
|
|
345
|
+
f" Secret key: {masked_secret}\n"
|
|
346
|
+
f" Base URL: {base_url}"
|
|
347
|
+
)
|
|
348
|
+
return "❌ api_key and/or secret_key not provided."
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
if __name__ == "__main__":
|
|
352
|
+
mcp.run(transport="streamable-http")
|