datacore-a2a-mcp 0.1.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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+
9
+ # Đã điền token thật — không bao giờ commit.
10
+ .mcp.json
11
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 3TIT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.5
2
+ Name: datacore-a2a-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server cho các skill dữ liệu DataCore: chuẩn hoá địa chỉ Việt Nam và tra cứu doanh nghiệp
5
+ Project-URL: Homepage, https://github.com/DataCore-VietNam/datacore-a2a-mcp
6
+ Project-URL: Documentation, https://github.com/DataCore-VietNam/datacore-a2a-mcp#readme
7
+ Project-URL: Privacy Policy, https://github.com/DataCore-VietNam/datacore-a2a-mcp/blob/master/PRIVACY.md
8
+ Project-URL: Issues, https://github.com/DataCore-VietNam/datacore-a2a-mcp/issues
9
+ Author-email: 3TIT <mcp-security@datacore.vn>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: a2a,address,company-data,mcp,model-context-protocol,vietnam
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+
22
+ # DataCore A2A — MCP server
23
+
24
+ <!-- mcp-name: vn.datacore/datacore-a2a -->
25
+ <!-- Marker dự phòng: registry tìm chuỗi con, nên hai dòng cùng tồn tại được.
26
+ Giữ đường lùi nếu xác minh tên miền không xong, khỏi phải phát hành
27
+ một version mới chỉ để sửa một dòng comment.
28
+ mcp-name: io.github.nambn3tit/datacore-a2a-mcp -->
29
+
30
+ Cho phép một MCP client (Claude Code, Claude Desktop, VS Code, Cursor…) dùng
31
+ các skill dữ liệu của DataCore như tool native: chuẩn hoá địa chỉ Việt Nam,
32
+ kiểm tra địa chỉ, tra cứu doanh nghiệp, sinh embedding.
33
+
34
+ ```
35
+ MCP client (stdio) ──► datacore-mcp ──HTTPS──► DataCore API
36
+ ```
37
+
38
+ Zero dependency, Python 3.9+.
39
+
40
+ ## Cài đặt
41
+
42
+ ```bash
43
+ pip install datacore-a2a-mcp
44
+ ```
45
+
46
+ ## Cần có trước
47
+
48
+ Server này là **client**, không phải dịch vụ. Nó cần một endpoint DataCore và
49
+ một mandate token để gọi — liên hệ 3TIT để được cấp. Không có hai thứ đó thì
50
+ server khởi động rồi thoát ngay kèm thông báo thiếu biến nào.
51
+
52
+ ## Cấu hình
53
+
54
+ | Biến | Bắt buộc | Ý nghĩa |
55
+ |---|---|---|
56
+ | `DATACORE_A2A_URL` | có | Base URL của endpoint DataCore được cấp cho bạn, ví dụ `https://api.example.com` |
57
+ | `DATACORE_MANDATE` | có | AP2 mandate token do DataCore cấp — quyết định skill nào được gọi và hạn mức chi tiêu |
58
+ | `DATACORE_SKILLS` | không | Danh sách skill id cách nhau bằng phẩy để giới hạn tool, hoặc `*` để lấy hết. Bỏ trống dùng mặc định |
59
+ | `DATACORE_TIMEOUT` | không | Timeout mỗi request (giây), mặc định `30` |
60
+
61
+ Dùng đúng URL DataCore cấp kèm mandate. Một endpoint khác sẽ không nhận mandate
62
+ của bạn và trả `401`.
63
+
64
+ ### Claude Code / Claude Desktop
65
+
66
+ Thêm vào `.mcp.json` (project) hoặc config của Claude Desktop:
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "datacore-a2a": {
72
+ "command": "datacore-mcp",
73
+ "env": {
74
+ "DATACORE_A2A_URL": "https://api.example.com",
75
+ "DATACORE_MANDATE": "<mandate-token>"
76
+ }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ ## Tool
83
+
84
+ Tool list được **sinh từ endpoint**, không hardcode ở đây — nên khi DataCore
85
+ thêm skill, restart server là thấy tool mới.
86
+
87
+ Mặc định expose 4 skill đã nối backend thật:
88
+
89
+ | Tool | Skill id | Việc nó làm |
90
+ |---|---|---|
91
+ | `address_normalize` | `address.normalize` | Chuẩn hoá địa chỉ Việt Nam thành tỉnh / huyện / xã |
92
+ | `address_validate` | `address.validate` | Kiểm tra một địa chỉ có tồn tại theo dữ liệu hành chính |
93
+ | `company_search` | `company.search` | Tra cứu doanh nghiệp theo tên hoặc mã số thuế |
94
+ | `company_embeddings` | `company.embeddings` | Sinh vector embedding cho văn bản |
95
+
96
+ Mọi tool là **read-only** (`readOnlyHint: true`): không ghi, không xoá, không
97
+ tạo gì. Gọi lại cùng input cho cùng kết quả.
98
+
99
+ Ba skill khác của DataCore (`company.graph`, `company.financial-sankey`,
100
+ `company.news`) **cố ý không được expose**: chúng đang trả dữ liệu
101
+ mock/deterministic. Muốn thấy chúng để thử thì đặt `DATACORE_SKILLS="*"`, và
102
+ đừng tin kết quả.
103
+
104
+ ## Ví dụ
105
+
106
+ Ba prompt dưới đây chạy qua ba tool khác nhau:
107
+
108
+ 1. **Chuẩn hoá địa chỉ** — "Chuẩn hoá địa chỉ này thành tỉnh/huyện/xã: So 1 Ly
109
+ Thai To, Hoan Kiem, Ha Noi"
110
+ 2. **Tra cứu doanh nghiệp** — "Tìm thông tin doanh nghiệp có mã số thuế
111
+ 0100109106 và cho tôi biết trạng thái hoạt động"
112
+ 3. **Embedding** — "Sinh embedding cho đoạn mô tả ngành nghề này rồi cho biết
113
+ vector có bao nhiêu chiều: bán buôn thiết bị điện tử"
114
+
115
+ ## Mô hình bảo mật
116
+
117
+ Server này mang mandate token **nguyên dạng** ở header `X-AP2-Mandate` để phía
118
+ DataCore verify, và không tự khai bất kỳ header authorization nào khác. Một
119
+ client không được là nơi tự cấp quyền cho chính nó — nó chỉ chuyển tiếp thứ nó
120
+ được cấp, còn mọi quyết định cho phép hay từ chối đều nằm ở phía DataCore.
121
+
122
+ Mandate là **bearer credential**: ai giữ nó cũng gọi được đúng những skill trong
123
+ đó, tới hết hạn mức, cho tới khi hết hạn. Giữ nó như giữ mật khẩu — đừng commit
124
+ `.mcp.json` đã điền token, và nếu client của bạn hỗ trợ, đọc nó từ trình quản lý
125
+ secret thay vì để inline.
126
+
127
+ **Package này** không bao giờ ghi token ra log, kể cả một phần; nội dung `text`
128
+ bạn gửi cũng không. Đó là cam kết cho tiến trình chạy trên máy bạn — phía dịch
129
+ vụ là chuyện khác, và [PRIVACY.md](PRIVACY.md) nói thẳng nó lưu những gì.
130
+
131
+ ## Xử lý sự cố
132
+
133
+ Log đi ra **stderr**, prefix `[datacore-mcp]`. stdout là kênh JSON-RPC.
134
+
135
+ | Triệu chứng | Nguyên nhân thường gặp |
136
+ |---|---|
137
+ | Server thoát ngay, `THIẾU biến môi trường bắt buộc` | Chưa set `DATACORE_A2A_URL` hoặc `DATACORE_MANDATE` |
138
+ | `401` mọi request | Mandate hết hạn, hoặc `DATACORE_A2A_URL` không phải endpoint đã cấp kèm mandate đó |
139
+ | `402` | Đã dùng hết spend cap của mandate |
140
+ | `403` | Skill nằm ngoài allowlist của mandate — xin mandate có skill đó |
141
+ | `502` | Backend của skill đó phía DataCore không phản hồi |
142
+ | Không thấy tool nào | `DATACORE_SKILLS` lọc hết, hoặc endpoint chưa đăng ký skill nào |
143
+
144
+ ## Báo lỗi bảo mật
145
+
146
+ `mcp-security@datacore.vn`. Đừng mở issue công khai cho lỗi bảo mật.
147
+
148
+ Nghi mandate của mình bị lộ thì **báo ngay** — đừng đợi tới lúc nó hết hạn.
149
+ Chúng tôi cần biết để xử lý phía dịch vụ.
150
+
151
+ ## Quyền riêng tư
152
+
153
+ Xem [PRIVACY.md](PRIVACY.md). Ngắn gọn: package này chạy trên máy bạn, không lưu
154
+ gì, không gửi đi đâu ngoài endpoint DataCore bạn tự cấu hình, và không bao giờ
155
+ ghi mandate token vào log.
156
+
157
+ ## License
158
+
159
+ MIT — xem [LICENSE](LICENSE).
@@ -0,0 +1,93 @@
1
+ # Privacy Policy — datacore-a2a-mcp
2
+
3
+ Áp dụng cho package `datacore-a2a-mcp` (MCP server). Cập nhật 2026-08-24.
4
+
5
+ ## Package này là gì, về mặt dữ liệu
6
+
7
+ `datacore-a2a-mcp` là một **client chạy trên máy bạn**. Nó không phải dịch vụ do
8
+ chúng tôi vận hành, không nhận dữ liệu về phía chúng tôi, và không có backend
9
+ riêng. Nó là một tiến trình cục bộ dịch giữa MCP client của bạn (Claude Code,
10
+ Claude Desktop, VS Code…) và endpoint DataCore mà **bạn** cấu hình.
11
+
12
+ ## Dữ liệu nó xử lý
13
+
14
+ | Dữ liệu | Từ đâu | Đi đâu |
15
+ |---|---|---|
16
+ | Nội dung `text` bạn (hoặc agent) đưa vào tool | MCP client | Gửi tới endpoint DataCore ở `DATACORE_A2A_URL` qua HTTPS |
17
+ | Skill id được gọi | Tên tool | Cùng request trên |
18
+ | Mandate token | `DATACORE_MANDATE` | Header `X-AP2-Mandate` của cùng request trên |
19
+ | Kết quả trả về | Endpoint DataCore | Trả lại MCP client |
20
+
21
+ Không có đích đến nào khác. Package **zero dependency** nên không có thư viện
22
+ bên thứ ba nào chạm vào dữ liệu, và không có telemetry, analytics, hay
23
+ crash-reporting nào.
24
+
25
+ ## Nó lưu gì
26
+
27
+ **Không lưu gì cả.** Không file, không cache, không database, không state giữa
28
+ các lần chạy. Mọi thứ nằm trong bộ nhớ tiến trình và mất khi tiến trình dừng.
29
+
30
+ ## Nó ghi log gì
31
+
32
+ Log đi ra **stderr**, nơi MCP client của bạn quyết định giữ hay bỏ:
33
+
34
+ - Lúc khởi động: base URL, timeout, danh sách skill được bật
35
+ - Lúc discovery: số tool tìm được và skill id của chúng
36
+ - Khi tool lỗi: câu lỗi đã được dịch, gồm mã HTTP và message của server
37
+
38
+ **Mandate token không bao giờ được ghi log, kể cả một phần của nó** — đây là
39
+ token cấp quyền chi tiêu, và stderr của MCP server thường được client ghi ra
40
+ file. Nội dung `text` bạn gửi cũng không được ghi log.
41
+
42
+ ## Dữ liệu gửi tới DataCore thì sao
43
+
44
+ Mục trên nói về package — thứ chạy trên máy bạn. Mục này nói về **dịch vụ** ở
45
+ đầu kia `DATACORE_A2A_URL`. Hai thứ khác nhau, và cam kết của package không phủ
46
+ lên dịch vụ được, nên phần này viết riêng.
47
+
48
+ ### Dịch vụ nhận gì
49
+
50
+ Mỗi lần gọi tool, dịch vụ nhận: nội dung `text` bạn gửi, skill id, mandate của
51
+ bạn (mandate id, client id, danh sách skill được phép, hạn mức chi tiêu, hạn sử
52
+ dụng), cùng những thứ mọi request HTTP đều mang — địa chỉ IP nguồn và header.
53
+
54
+ ### `text` của bạn đi tới đâu
55
+
56
+ Gateway verify chữ ký mandate → orchestrator kiểm skill có nằm trong mandate
57
+ không và hạn mức còn không → chuyển cho worker agent của skill đó → worker gọi
58
+ API dữ liệu tương ứng: dữ liệu địa giới hành chính, dữ liệu doanh nghiệp, hoặc
59
+ mô hình embedding.
60
+
61
+ Các API dữ liệu đó là hệ thống riêng, không cùng một hệ thống với gateway. Nên
62
+ **nội dung `text` của bạn có rời khỏi phạm vi chúng tôi để tới bên cung cấp dữ
63
+ liệu** — đó là cách duy nhất để có câu trả lời cho truy vấn của bạn. Bên cung
64
+ cấp dữ liệu có chính sách riêng của họ.
65
+
66
+ ### Dịch vụ lưu gì
67
+
68
+ | Thứ được lưu | Nội dung | Sống bao lâu |
69
+ |---|---|---|
70
+ | Bộ đếm chi tiêu | Một con số cho mỗi mandate. Không có nội dung truy vấn | Tự xoá đúng lúc mandate hết hạn |
71
+ | Bản ghi tính phí | mandate id, client id, worker, skill id, số tiền, thời điểm. Không có nội dung truy vấn | Theo hệ thống tính phí |
72
+ | Log request ở gateway | **Body request — tức `text` của bạn — body response, header, query string, IP nguồn** | Theo chính sách log của hệ thống |
73
+
74
+ Dòng thứ ba là dòng đáng đọc kỹ: gateway ghi log ở mức đầy đủ, nên **truy vấn
75
+ và kết quả của bạn được lưu lại ở phía dịch vụ, không chỉ đi ngang qua**. Nếu
76
+ định gửi dữ liệu nhạy cảm qua tool này, hãy tính trên giả định đó. Package
77
+ không hứa thay cho phần này được — nó chỉ hứa được rằng bản thân nó không lưu
78
+ và không ghi gì (mục trên).
79
+
80
+ ### Dùng để làm gì
81
+
82
+ Phục vụ chính request đó, cộng với vận hành và tính phí: đối soát khi có sự cố,
83
+ đếm chi tiêu theo mandate, xuất hoá đơn. Không bán cho ai, và không dùng nội
84
+ dung truy vấn của bạn để huấn luyện mô hình.
85
+
86
+ ### Yêu cầu xoá, hoặc khiếu nại
87
+
88
+ `mcp-security@datacore.vn`. Nói rõ mandate id hoặc client id để chúng tôi tìm
89
+ đúng bản ghi — chúng tôi không tra được theo nội dung truy vấn.
90
+
91
+ ## Liên hệ
92
+
93
+ Vấn đề bảo mật hoặc quyền riêng tư: `mcp-security@datacore.vn`
@@ -0,0 +1,138 @@
1
+ # DataCore A2A — MCP server
2
+
3
+ <!-- mcp-name: vn.datacore/datacore-a2a -->
4
+ <!-- Marker dự phòng: registry tìm chuỗi con, nên hai dòng cùng tồn tại được.
5
+ Giữ đường lùi nếu xác minh tên miền không xong, khỏi phải phát hành
6
+ một version mới chỉ để sửa một dòng comment.
7
+ mcp-name: io.github.nambn3tit/datacore-a2a-mcp -->
8
+
9
+ Cho phép một MCP client (Claude Code, Claude Desktop, VS Code, Cursor…) dùng
10
+ các skill dữ liệu của DataCore như tool native: chuẩn hoá địa chỉ Việt Nam,
11
+ kiểm tra địa chỉ, tra cứu doanh nghiệp, sinh embedding.
12
+
13
+ ```
14
+ MCP client (stdio) ──► datacore-mcp ──HTTPS──► DataCore API
15
+ ```
16
+
17
+ Zero dependency, Python 3.9+.
18
+
19
+ ## Cài đặt
20
+
21
+ ```bash
22
+ pip install datacore-a2a-mcp
23
+ ```
24
+
25
+ ## Cần có trước
26
+
27
+ Server này là **client**, không phải dịch vụ. Nó cần một endpoint DataCore và
28
+ một mandate token để gọi — liên hệ 3TIT để được cấp. Không có hai thứ đó thì
29
+ server khởi động rồi thoát ngay kèm thông báo thiếu biến nào.
30
+
31
+ ## Cấu hình
32
+
33
+ | Biến | Bắt buộc | Ý nghĩa |
34
+ |---|---|---|
35
+ | `DATACORE_A2A_URL` | có | Base URL của endpoint DataCore được cấp cho bạn, ví dụ `https://api.example.com` |
36
+ | `DATACORE_MANDATE` | có | AP2 mandate token do DataCore cấp — quyết định skill nào được gọi và hạn mức chi tiêu |
37
+ | `DATACORE_SKILLS` | không | Danh sách skill id cách nhau bằng phẩy để giới hạn tool, hoặc `*` để lấy hết. Bỏ trống dùng mặc định |
38
+ | `DATACORE_TIMEOUT` | không | Timeout mỗi request (giây), mặc định `30` |
39
+
40
+ Dùng đúng URL DataCore cấp kèm mandate. Một endpoint khác sẽ không nhận mandate
41
+ của bạn và trả `401`.
42
+
43
+ ### Claude Code / Claude Desktop
44
+
45
+ Thêm vào `.mcp.json` (project) hoặc config của Claude Desktop:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "datacore-a2a": {
51
+ "command": "datacore-mcp",
52
+ "env": {
53
+ "DATACORE_A2A_URL": "https://api.example.com",
54
+ "DATACORE_MANDATE": "<mandate-token>"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Tool
62
+
63
+ Tool list được **sinh từ endpoint**, không hardcode ở đây — nên khi DataCore
64
+ thêm skill, restart server là thấy tool mới.
65
+
66
+ Mặc định expose 4 skill đã nối backend thật:
67
+
68
+ | Tool | Skill id | Việc nó làm |
69
+ |---|---|---|
70
+ | `address_normalize` | `address.normalize` | Chuẩn hoá địa chỉ Việt Nam thành tỉnh / huyện / xã |
71
+ | `address_validate` | `address.validate` | Kiểm tra một địa chỉ có tồn tại theo dữ liệu hành chính |
72
+ | `company_search` | `company.search` | Tra cứu doanh nghiệp theo tên hoặc mã số thuế |
73
+ | `company_embeddings` | `company.embeddings` | Sinh vector embedding cho văn bản |
74
+
75
+ Mọi tool là **read-only** (`readOnlyHint: true`): không ghi, không xoá, không
76
+ tạo gì. Gọi lại cùng input cho cùng kết quả.
77
+
78
+ Ba skill khác của DataCore (`company.graph`, `company.financial-sankey`,
79
+ `company.news`) **cố ý không được expose**: chúng đang trả dữ liệu
80
+ mock/deterministic. Muốn thấy chúng để thử thì đặt `DATACORE_SKILLS="*"`, và
81
+ đừng tin kết quả.
82
+
83
+ ## Ví dụ
84
+
85
+ Ba prompt dưới đây chạy qua ba tool khác nhau:
86
+
87
+ 1. **Chuẩn hoá địa chỉ** — "Chuẩn hoá địa chỉ này thành tỉnh/huyện/xã: So 1 Ly
88
+ Thai To, Hoan Kiem, Ha Noi"
89
+ 2. **Tra cứu doanh nghiệp** — "Tìm thông tin doanh nghiệp có mã số thuế
90
+ 0100109106 và cho tôi biết trạng thái hoạt động"
91
+ 3. **Embedding** — "Sinh embedding cho đoạn mô tả ngành nghề này rồi cho biết
92
+ vector có bao nhiêu chiều: bán buôn thiết bị điện tử"
93
+
94
+ ## Mô hình bảo mật
95
+
96
+ Server này mang mandate token **nguyên dạng** ở header `X-AP2-Mandate` để phía
97
+ DataCore verify, và không tự khai bất kỳ header authorization nào khác. Một
98
+ client không được là nơi tự cấp quyền cho chính nó — nó chỉ chuyển tiếp thứ nó
99
+ được cấp, còn mọi quyết định cho phép hay từ chối đều nằm ở phía DataCore.
100
+
101
+ Mandate là **bearer credential**: ai giữ nó cũng gọi được đúng những skill trong
102
+ đó, tới hết hạn mức, cho tới khi hết hạn. Giữ nó như giữ mật khẩu — đừng commit
103
+ `.mcp.json` đã điền token, và nếu client của bạn hỗ trợ, đọc nó từ trình quản lý
104
+ secret thay vì để inline.
105
+
106
+ **Package này** không bao giờ ghi token ra log, kể cả một phần; nội dung `text`
107
+ bạn gửi cũng không. Đó là cam kết cho tiến trình chạy trên máy bạn — phía dịch
108
+ vụ là chuyện khác, và [PRIVACY.md](PRIVACY.md) nói thẳng nó lưu những gì.
109
+
110
+ ## Xử lý sự cố
111
+
112
+ Log đi ra **stderr**, prefix `[datacore-mcp]`. stdout là kênh JSON-RPC.
113
+
114
+ | Triệu chứng | Nguyên nhân thường gặp |
115
+ |---|---|
116
+ | Server thoát ngay, `THIẾU biến môi trường bắt buộc` | Chưa set `DATACORE_A2A_URL` hoặc `DATACORE_MANDATE` |
117
+ | `401` mọi request | Mandate hết hạn, hoặc `DATACORE_A2A_URL` không phải endpoint đã cấp kèm mandate đó |
118
+ | `402` | Đã dùng hết spend cap của mandate |
119
+ | `403` | Skill nằm ngoài allowlist của mandate — xin mandate có skill đó |
120
+ | `502` | Backend của skill đó phía DataCore không phản hồi |
121
+ | Không thấy tool nào | `DATACORE_SKILLS` lọc hết, hoặc endpoint chưa đăng ký skill nào |
122
+
123
+ ## Báo lỗi bảo mật
124
+
125
+ `mcp-security@datacore.vn`. Đừng mở issue công khai cho lỗi bảo mật.
126
+
127
+ Nghi mandate của mình bị lộ thì **báo ngay** — đừng đợi tới lúc nó hết hạn.
128
+ Chúng tôi cần biết để xử lý phía dịch vụ.
129
+
130
+ ## Quyền riêng tư
131
+
132
+ Xem [PRIVACY.md](PRIVACY.md). Ngắn gọn: package này chạy trên máy bạn, không lưu
133
+ gì, không gửi đi đâu ngoài endpoint DataCore bạn tự cấu hình, và không bao giờ
134
+ ghi mandate token vào log.
135
+
136
+ ## License
137
+
138
+ MIT — xem [LICENSE](LICENSE).
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ # Tên xác nhận còn trống 2026-08-24 (pypi.org/pypi/datacore-a2a-mcp/json → 404).
7
+ # Tên trên PyPI là vĩnh viễn, không đổi được sau khi đã upload.
8
+ name = "datacore-a2a-mcp"
9
+ dynamic = ["version"]
10
+ description = "MCP server cho các skill dữ liệu DataCore: chuẩn hoá địa chỉ Việt Nam và tra cứu doanh nghiệp"
11
+ readme = "README.md"
12
+ requires-python = ">=3.9"
13
+
14
+ # Cố ý rỗng. Zero dependency là tính năng, không phải thiếu sót: MCP stdio chỉ
15
+ # là JSON-RPC 2.0 phân cách bằng newline, nên `pip install` không kéo theo gì
16
+ # và cũng không có dependency nào để bị CVE.
17
+ dependencies = []
18
+
19
+ keywords = ["mcp", "model-context-protocol", "a2a", "vietnam", "address", "company-data"]
20
+
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Environment :: Console",
24
+ "Intended Audience :: Developers",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3 :: Only",
27
+ "Topic :: Software Development :: Libraries",
28
+ ]
29
+
30
+ # MIT: package này là một wrapper mỏng không chứa business logic, nên điều khoản
31
+ # patent của Apache-2.0 không mua thêm gì đáng kể, và MIT thì ngắn, quen mắt,
32
+ # không làm ai phải đọc kỹ trước khi cài. Đổi sang Apache-2.0 nếu muốn có
33
+ # patent grant tường minh — chỉ cần thay file LICENSE và dòng dưới.
34
+ #
35
+ # Dạng SPDX string này cần hatchling >= 1.27 (PEP 639). Build lỗi ở chỗ này thì
36
+ # đổi về: license = { file = "LICENSE" } và thêm lại classifier
37
+ # "License :: OSI Approved :: MIT License".
38
+ license = "MIT"
39
+ license-files = ["LICENSE"]
40
+
41
+ [[project.authors]]
42
+ name = "3TIT"
43
+ # ⚠️ Hộp thư này phải tồn tại thật trước khi publish — PyPI và registry đều hiện
44
+ # nó ra, và đây là đường người ta báo lỗi bảo mật. Alias vào đâu cũng được.
45
+ email = "mcp-security@datacore.vn"
46
+
47
+ [project.scripts]
48
+ datacore-mcp = "datacore_mcp.server:main"
49
+
50
+ [project.urls]
51
+ Homepage = "https://github.com/DataCore-VietNam/datacore-a2a-mcp"
52
+ Documentation = "https://github.com/DataCore-VietNam/datacore-a2a-mcp#readme"
53
+ # PRIVACY.md trong repo, không phải một trang riêng — đủ cho Anthropic và không
54
+ # thêm một chỗ nữa để quên cập nhật. Phần nói về package đã đầy đủ; phần nói về
55
+ # dịch vụ DataCore còn một khối TODO phải điền trước khi submit Anthropic.
56
+ "Privacy Policy" = "https://github.com/DataCore-VietNam/datacore-a2a-mcp/blob/master/PRIVACY.md"
57
+ Issues = "https://github.com/DataCore-VietNam/datacore-a2a-mcp/issues"
58
+
59
+ [tool.hatch.version]
60
+ path = "src/datacore_mcp/__init__.py"
61
+
62
+ [tool.hatch.build.targets.wheel]
63
+ packages = ["src/datacore_mcp"]
64
+
65
+ [tool.hatch.build.targets.sdist]
66
+ # sdist mà PyPI phát ra chứa đúng những gì liệt kê ở đây. Giữ danh sách hẹp:
67
+ # mọi file lọt vào đây là file công khai.
68
+ #
69
+ # PRIVACY.md có trong danh sách vì người chỉ tải sdist vẫn phải đọc được nó.
70
+ # Runbook phát hành cố ý không nằm trong repo này — nó là tài liệu nội bộ, không
71
+ # phải tài liệu cho người dùng package.
72
+ include = ["src/datacore_mcp", "README.md", "PRIVACY.md", "pyproject.toml"]
@@ -0,0 +1,3 @@
1
+ """MCP server cho các skill dữ liệu của DataCore A2A."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP server cho DataCore: mỗi skill dữ liệu thành một MCP tool.
4
+
5
+ MCP client (stdio) ──► server này ──HTTPS──► DataCore API
6
+
7
+ Mandate token được mang **nguyên dạng** ở header ``X-AP2-Mandate`` để phía
8
+ DataCore verify. Server này không bao giờ tự khai bất kỳ header authorization
9
+ nào khác: một client không được là nơi tự cấp quyền cho chính nó, nên nó chỉ
10
+ chuyển tiếp thứ nó được cấp.
11
+
12
+ Tool list sinh **từ endpoint** chứ không hardcode ở đây, nên DataCore thêm
13
+ skill là client thấy tool mới sau khi restart server này.
14
+
15
+ Zero dependency, Python 3.9+. MCP stdio chỉ là JSON-RPC 2.0 phân cách bằng
16
+ newline nên không cần SDK — và quan trọng hơn, người cài không phải kéo theo
17
+ cây dependency nào.
18
+ """
19
+
20
+ import json
21
+ import os
22
+ import re
23
+ import sys
24
+ import urllib.error
25
+ import urllib.request
26
+ import uuid
27
+
28
+ try:
29
+ from . import __version__
30
+ except ImportError: # chạy trực tiếp file này, ngoài package
31
+ __version__ = "0.0.0+local"
32
+
33
+
34
+ # ------------------------------------------------------------------ cấu hình
35
+
36
+ #: Skill được expose ra tool. Cố ý là danh sách trắng tường minh chứ không
37
+ #: phải "mọi thứ endpoint trả về": ``company.graph``,
38
+ #: ``company.financial-sankey`` và ``company.news`` hiện trả dữ liệu
39
+ #: demo/deterministic, nên đưa chúng ra ngoài là mô tả dữ liệu giả như dữ liệu
40
+ #: thật — lý do bị registry từ chối phổ biến nhất. Chuyển sang đây khi từng
41
+ #: skill có dữ liệu thật.
42
+ DEFAULT_SKILLS = (
43
+ "address.normalize",
44
+ "address.validate",
45
+ "company.search",
46
+ "company.embeddings",
47
+ )
48
+
49
+ PROTOCOL_FALLBACK = "2025-06-18"
50
+
51
+ # Tên tool MCP không nhận dấu chấm, mà skill id thì có ("address.normalize").
52
+ # Đổi sang '_' và giữ map ngược lại — cái gửi lên phải là id thật.
53
+ _TOOL_NAME_SAFE = re.compile(r"[^a-zA-Z0-9_-]")
54
+
55
+ _skill_by_tool_name = {}
56
+
57
+
58
+ def log(msg):
59
+ """stderr, không phải stdout — stdout là kênh JSON-RPC, ghi gì vào đó là hỏng protocol."""
60
+ print(f"[datacore-mcp] {msg}", file=sys.stderr, flush=True)
61
+
62
+
63
+ def _require(name, what, example):
64
+ """
65
+ Env bắt buộc, không có default. Bản trước default ``A2A_BASE_URL`` về một
66
+ IP LAN của người viết; ai cài cũng gặp timeout mà không hiểu vì sao. Thiếu
67
+ cấu hình là lỗi không tự phục hồi được, nên fail ngay và nói rõ thiếu gì.
68
+ """
69
+ value = os.environ.get(name, "").strip()
70
+ if not value:
71
+ log(f"THIẾU biến môi trường bắt buộc {name} ({what}).")
72
+ log(f" ví dụ: {name}={example}")
73
+ log(" xem README để biết cách lấy mandate và URL endpoint.")
74
+ sys.exit(2)
75
+ return value
76
+
77
+
78
+ def load_config():
79
+ base_url = _require(
80
+ "DATACORE_A2A_URL",
81
+ "base URL của endpoint DataCore",
82
+ "https://api.example.com",
83
+ ).rstrip("/")
84
+ mandate = _require(
85
+ "DATACORE_MANDATE",
86
+ "AP2 mandate token do DataCore cấp",
87
+ "<jws-token>",
88
+ )
89
+
90
+ raw_skills = os.environ.get("DATACORE_SKILLS", "").strip()
91
+ if raw_skills == "*":
92
+ skills = None # None = không lọc, lấy hết những gì endpoint trả về
93
+ elif raw_skills:
94
+ skills = tuple(s.strip() for s in raw_skills.split(",") if s.strip())
95
+ else:
96
+ skills = DEFAULT_SKILLS
97
+
98
+ timeout = float(os.environ.get("DATACORE_TIMEOUT", "30"))
99
+ return base_url, mandate, skills, timeout
100
+
101
+
102
+ BASE_URL, MANDATE, SKILL_FILTER, TIMEOUT = "", "", DEFAULT_SKILLS, 30.0
103
+
104
+
105
+ # ------------------------------------------------------------------ HTTP
106
+
107
+
108
+ class A2AError(Exception):
109
+ """DataCore trả về HTTP lỗi. ``status`` là mã, ``detail`` là body."""
110
+
111
+ def __init__(self, status, detail):
112
+ super().__init__(f"HTTP {status}: {detail}")
113
+ self.status = status
114
+ self.detail = detail
115
+
116
+
117
+ #: Mã status DataCore trả về, dịch sang câu người đọc hiểu được thay vì để
118
+ #: client tự đoán ý nghĩa của một con số như 402.
119
+ _STATUS_HINT = {
120
+ 400: "request không hợp lệ, hoặc skill chưa được đăng ký",
121
+ 401: (
122
+ "mandate thiếu hoặc không hợp lệ. Kiểm tra DATACORE_MANDATE còn hạn, và "
123
+ "kiểm tra DATACORE_A2A_URL đúng là endpoint DataCore đã được cấp"
124
+ ),
125
+ 402: "vượt spend cap của mandate — cần mandate mới hoặc cap cao hơn",
126
+ 403: "skill nằm ngoài allowlist của mandate",
127
+ 404: "không tìm thấy route — kiểm tra lại DATACORE_A2A_URL",
128
+ 502: "backend của skill này không phản hồi",
129
+ 503: "dịch vụ tạm thời không khả dụng",
130
+ }
131
+
132
+
133
+ def _error_detail(exc):
134
+ """
135
+ Lấy field ``error`` trong body lỗi. Body có thể không phải JSON (một proxy
136
+ chen vào và trả HTML chẳng hạn), nên phải chịu được cả trường hợp đó thay
137
+ vì ném tiếp một lỗi parse che mất lỗi gốc.
138
+ """
139
+ try:
140
+ body = exc.read().decode("utf-8", "replace")
141
+ except Exception:
142
+ return exc.reason or "không có chi tiết"
143
+ try:
144
+ parsed = json.loads(body)
145
+ except ValueError:
146
+ return (body[:300] or exc.reason or "không có chi tiết").strip()
147
+ if isinstance(parsed, dict):
148
+ return parsed.get("error") or parsed.get("message") or json.dumps(parsed)[:300]
149
+ return body[:300]
150
+
151
+
152
+ def http_json(path, payload=None):
153
+ """
154
+ Một chỗ duy nhất gắn ``X-AP2-Mandate``. Token đi cả trên discovery lẫn
155
+ invoke, vì cả hai đều là request được authorize — discovery không mang
156
+ mandate cũng bị từ chối 401.
157
+ """
158
+ data = json.dumps(payload).encode() if payload is not None else None
159
+ headers = {"Accept": "application/json", "X-AP2-Mandate": MANDATE}
160
+ if data is not None:
161
+ headers["Content-Type"] = "application/json"
162
+
163
+ req = urllib.request.Request(
164
+ f"{BASE_URL}{path}",
165
+ data=data,
166
+ headers=headers,
167
+ method="POST" if data is not None else "GET",
168
+ )
169
+ try:
170
+ with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
171
+ return json.loads(resp.read().decode())
172
+ except urllib.error.HTTPError as e:
173
+ raise A2AError(e.code, _error_detail(e)) from None
174
+ except urllib.error.URLError as e:
175
+ raise RuntimeError(f"không kết nối được tới {BASE_URL}: {e.reason}") from None
176
+
177
+
178
+ # ------------------------------------------------------------------ discovery
179
+
180
+
181
+ def tool_name_for(skill_id):
182
+ return _TOOL_NAME_SAFE.sub("_", skill_id)
183
+
184
+
185
+ def discover_tools():
186
+ """
187
+ Sinh tool list từ endpoint discovery.
188
+
189
+ Endpoint trả kèm vài field mô tả hạ tầng phía DataCore; chúng bị bỏ đi có
190
+ chủ ý. Địa chỉ nội bộ và thông điệp lỗi hạ tầng không có việc gì phải đi
191
+ tới máy người dùng, và càng không thuộc về một tool description mà model
192
+ sẽ đọc.
193
+ """
194
+ entries = http_json("/orchestrate/skills")
195
+ if not isinstance(entries, list):
196
+ raise RuntimeError(f"/orchestrate/skills trả về {type(entries).__name__}, cần một list")
197
+
198
+ _skill_by_tool_name.clear()
199
+ tools = []
200
+ skipped = []
201
+
202
+ for entry in entries:
203
+ skill_id = (entry or {}).get("id")
204
+ if not skill_id:
205
+ continue
206
+ if SKILL_FILTER is not None and skill_id not in SKILL_FILTER:
207
+ skipped.append(skill_id)
208
+ continue
209
+
210
+ name = tool_name_for(skill_id)
211
+ _skill_by_tool_name[name] = skill_id
212
+
213
+ # `name` của endpoint là mô tả ngắn của chính skill; nó là title hiển
214
+ # thị tốt hơn skill id. Rơi về id khi endpoint không trả được mô tả.
215
+ title = entry.get("name") or skill_id
216
+ description = entry.get("description") or f"DataCore skill {skill_id}"
217
+ examples = entry.get("examples") or []
218
+ if examples:
219
+ description += "\n\nVí dụ input: " + "; ".join(str(e) for e in examples[:3])
220
+ if entry.get("available") is False:
221
+ description += (
222
+ "\n\n(Agent giữ skill này đang không phản hồi. Skill id vẫn đúng "
223
+ "và gọi được khi agent trở lại.)"
224
+ )
225
+
226
+ tools.append({
227
+ "name": name,
228
+ "title": title,
229
+ "description": description,
230
+ # Bốn hint đều là sự thật về skill, không phải khai cho đủ: mọi
231
+ # skill hiện tại là tra cứu thuần, không ghi gì, gọi lại cùng input
232
+ # cho cùng kết quả, và dữ liệu đến từ backend ngoài process này.
233
+ "annotations": {
234
+ "title": title,
235
+ "readOnlyHint": True,
236
+ "destructiveHint": False,
237
+ "idempotentHint": True,
238
+ "openWorldHint": True,
239
+ },
240
+ "inputSchema": {
241
+ "type": "object",
242
+ "properties": {
243
+ "text": {
244
+ "type": "string",
245
+ "description": "Input dạng free-text gửi cho skill.",
246
+ }
247
+ },
248
+ "required": ["text"],
249
+ "additionalProperties": False,
250
+ },
251
+ })
252
+
253
+ log(f"{len(tools)} tool từ {BASE_URL}: {sorted(_skill_by_tool_name.values())}")
254
+ if skipped:
255
+ log(f"bỏ qua {len(skipped)} skill ngoài DATACORE_SKILLS: {sorted(skipped)}")
256
+ return tools
257
+
258
+
259
+ def call_skill(skill_id, text):
260
+ """Gọi một skill và trả về payload thô mà DataCore trả lại."""
261
+ body = http_json("/orchestrate", {"skill": skill_id, "text": text})
262
+
263
+ payload = (body or {}).get("payload")
264
+ state = (body or {}).get("state")
265
+ if not payload:
266
+ raise RuntimeError(f"DataCore trả state={state} nhưng không có payload")
267
+ return payload
268
+
269
+
270
+ # ------------------------------------------------------------------ MCP
271
+
272
+
273
+ def _tool_error(exc):
274
+ """Câu lỗi cho Claude đọc: mã status → nguyên nhân, kèm chi tiết của server."""
275
+ if isinstance(exc, A2AError):
276
+ hint = _STATUS_HINT.get(exc.status, "lỗi từ DataCore")
277
+ return f"{hint} (HTTP {exc.status}): {exc.detail}"
278
+ return str(exc)
279
+
280
+
281
+ def handle(method, params):
282
+ """
283
+ Trả về ``(result, error)``. Chỉ implement những method client thực sự gọi
284
+ — initialize / tools/list / tools/call — cộng ping. Method khác trả -32601.
285
+ """
286
+ if method == "initialize":
287
+ requested = params.get("protocolVersion")
288
+ return {
289
+ # Echo lại version client xin nếu là string: server không dùng gì
290
+ # đặc thù theo version, nên khớp client là lựa chọn an toàn nhất.
291
+ "protocolVersion": requested if isinstance(requested, str) else PROTOCOL_FALLBACK,
292
+ "capabilities": {"tools": {"listChanged": False}},
293
+ "serverInfo": {"name": "datacore-a2a", "version": __version__},
294
+ }, None
295
+
296
+ if method == "ping":
297
+ return {}, None
298
+
299
+ if method == "tools/list":
300
+ try:
301
+ return {"tools": discover_tools()}, None
302
+ except Exception as e:
303
+ # Không với tới endpoint được là lỗi cấu hình, không phải lỗi
304
+ # protocol — trả error để client hiện lý do thật.
305
+ return None, {"code": -32603, "message": f"Discovery thất bại: {_tool_error(e)}"}
306
+
307
+ if method == "tools/call":
308
+ name = params.get("name")
309
+ args = params.get("arguments") or {}
310
+ text = args.get("text")
311
+
312
+ if not _skill_by_tool_name:
313
+ try:
314
+ discover_tools()
315
+ except Exception as e:
316
+ return None, {"code": -32603, "message": f"Discovery thất bại: {_tool_error(e)}"}
317
+
318
+ skill_id = _skill_by_tool_name.get(name)
319
+ if not skill_id:
320
+ return None, {"code": -32602, "message": f"Tool không tồn tại: {name}"}
321
+ if not isinstance(text, str) or not text.strip():
322
+ return None, {"code": -32602, "message": "Thiếu tham số 'text'"}
323
+
324
+ try:
325
+ return {"content": [{"type": "text", "text": call_skill(skill_id, text)}]}, None
326
+ except Exception as e:
327
+ # isError=true để Claude thấy tool thất bại và tự điều chỉnh, thay
328
+ # vì tưởng chuỗi lỗi là kết quả hợp lệ.
329
+ message = _tool_error(e)
330
+ log(f"tool {name} lỗi: {message}")
331
+ return {"content": [{"type": "text", "text": message}], "isError": True}, None
332
+
333
+ return None, {"code": -32601, "message": f"Method không hỗ trợ: {method}"}
334
+
335
+
336
+ def serve():
337
+ for line in sys.stdin:
338
+ line = line.strip()
339
+ if not line:
340
+ continue
341
+ try:
342
+ request = json.loads(line)
343
+ except json.JSONDecodeError as e:
344
+ log(f"bỏ qua dòng không phải JSON: {e}")
345
+ continue
346
+
347
+ req_id = request.get("id")
348
+ method = request.get("method")
349
+
350
+ # Notification (không có id) — theo spec KHÔNG được trả lời.
351
+ # notifications/initialized rơi vào đây.
352
+ if req_id is None:
353
+ log(f"notification: {method}")
354
+ continue
355
+
356
+ try:
357
+ result, error = handle(method, request.get("params") or {})
358
+ except Exception as e:
359
+ result, error = None, {"code": -32603, "message": f"Lỗi nội bộ: {e}"}
360
+
361
+ response = {"jsonrpc": "2.0", "id": req_id}
362
+ if error:
363
+ response["error"] = error
364
+ else:
365
+ response["result"] = result
366
+
367
+ sys.stdout.write(json.dumps(response) + "\n")
368
+ sys.stdout.flush()
369
+
370
+
371
+ def main():
372
+ global BASE_URL, MANDATE, SKILL_FILTER, TIMEOUT
373
+ BASE_URL, MANDATE, SKILL_FILTER, TIMEOUT = load_config()
374
+
375
+ # Không log MANDATE, kể cả một phần của nó: đây là token cấp quyền chi
376
+ # tiêu, và stderr của MCP server thường được client ghi ra file log.
377
+ scope = "tất cả skill của endpoint" if SKILL_FILTER is None else ", ".join(SKILL_FILTER)
378
+ log(f"start v{__version__} | base={BASE_URL} | timeout={TIMEOUT}s | skill={scope}")
379
+
380
+ try:
381
+ serve()
382
+ except (KeyboardInterrupt, BrokenPipeError):
383
+ pass
384
+
385
+
386
+ if __name__ == "__main__":
387
+ main()