iso20022-mcp 0.0.1__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.
- iso20022_mcp-0.0.1/LICENSE +189 -0
- iso20022_mcp-0.0.1/PKG-INFO +159 -0
- iso20022_mcp-0.0.1/README.md +127 -0
- iso20022_mcp-0.0.1/iso20022_mcp/__init__.py +18 -0
- iso20022_mcp-0.0.1/iso20022_mcp/registry.py +262 -0
- iso20022_mcp-0.0.1/iso20022_mcp/server.py +218 -0
- iso20022_mcp-0.0.1/pyproject.toml +156 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work.
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
42
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
43
|
+
of this License, Derivative Works shall not include works that remain
|
|
44
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
45
|
+
the Work and Derivative Works thereof.
|
|
46
|
+
|
|
47
|
+
"Contribution" shall mean any work of authorship, including
|
|
48
|
+
the original version of the Work and any modifications or additions
|
|
49
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
50
|
+
submitted to the Licensor for inclusion in the Work by the copyright owner
|
|
51
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
52
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
53
|
+
means any form of electronic, verbal, or written communication sent
|
|
54
|
+
to the Licensor or its representatives, including but not limited to
|
|
55
|
+
communication on electronic mailing lists, source code control systems,
|
|
56
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
57
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
58
|
+
excluding communication that is conspicuously marked or otherwise
|
|
59
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
60
|
+
|
|
61
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
62
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
63
|
+
subsequently incorporated within the Work.
|
|
64
|
+
|
|
65
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
66
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
67
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
68
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
69
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
70
|
+
Work and such Derivative Works in Source or Object form.
|
|
71
|
+
|
|
72
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
73
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
74
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
75
|
+
(except as stated in this section) patent license to make, have made,
|
|
76
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
77
|
+
where such license applies only to those patent claims licensable
|
|
78
|
+
by such Contributor that are necessarily infringed by their
|
|
79
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
80
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
81
|
+
institute patent litigation against any entity (including a
|
|
82
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
83
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
84
|
+
or contributory patent infringement, then any patent licenses
|
|
85
|
+
granted to You under this License for that Work shall terminate
|
|
86
|
+
as of the date such litigation is filed.
|
|
87
|
+
|
|
88
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
89
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
90
|
+
modifications, and in Source or Object form, provided that You
|
|
91
|
+
meet the following conditions:
|
|
92
|
+
|
|
93
|
+
(a) You must give any other recipients of the Work or
|
|
94
|
+
Derivative Works a copy of this License; and
|
|
95
|
+
|
|
96
|
+
(b) You must cause any modified files to carry prominent notices
|
|
97
|
+
stating that You changed the files; and
|
|
98
|
+
|
|
99
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
100
|
+
that You distribute, all copyright, patent, trademark, and
|
|
101
|
+
attribution notices from the Source form of the Work,
|
|
102
|
+
excluding those notices that do not pertain to any part of
|
|
103
|
+
the Derivative Works; and
|
|
104
|
+
|
|
105
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
106
|
+
distribution, then any Derivative Works that You distribute must
|
|
107
|
+
include a readable copy of the attribution notices contained
|
|
108
|
+
within such NOTICE file, excluding any notices that do not
|
|
109
|
+
pertain to any part of the Derivative Works, in at least one
|
|
110
|
+
of the following places: within a NOTICE text file distributed
|
|
111
|
+
as part of the Derivative Works; within the Source form or
|
|
112
|
+
documentation, if provided along with the Derivative Works; or,
|
|
113
|
+
within a display generated by the Derivative Works, if and
|
|
114
|
+
wherever such third-party notices normally appear. The contents
|
|
115
|
+
of the NOTICE file are for informational purposes only and
|
|
116
|
+
do not modify the License. You may add Your own attribution
|
|
117
|
+
notices within Derivative Works that You distribute, alongside
|
|
118
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
119
|
+
that such additional attribution notices cannot be construed
|
|
120
|
+
as modifying the License.
|
|
121
|
+
|
|
122
|
+
You may add Your own copyright statement to Your modifications and
|
|
123
|
+
may provide additional or different license terms and conditions
|
|
124
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
125
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
126
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
127
|
+
the conditions stated in this License.
|
|
128
|
+
|
|
129
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
130
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
131
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
132
|
+
this License, without any additional terms or conditions.
|
|
133
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
134
|
+
the terms of any separate license agreement you may have executed
|
|
135
|
+
with Licensor regarding such Contributions.
|
|
136
|
+
|
|
137
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
138
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
139
|
+
except as required for reasonable and customary use in describing the
|
|
140
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
141
|
+
|
|
142
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
143
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
144
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
145
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
146
|
+
implied, including, without limitation, any warranties or conditions
|
|
147
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
148
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
149
|
+
appropriateness of using or redistributing the Work and assume any
|
|
150
|
+
risks associated with Your exercise of permissions under this License.
|
|
151
|
+
|
|
152
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
153
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
154
|
+
unless required by applicable law (such as deliberate and grossly
|
|
155
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
156
|
+
liable to You for damages, including any direct, indirect, special,
|
|
157
|
+
incidental, or consequential damages of any character arising as a
|
|
158
|
+
result of this License or out of the use or inability to use the
|
|
159
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
160
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
161
|
+
other commercial damages or losses), even if such Contributor
|
|
162
|
+
has been advised of the possibility of such damages.
|
|
163
|
+
|
|
164
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
165
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
166
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
167
|
+
or other liability obligations and/or rights consistent with this
|
|
168
|
+
License. However, in accepting such obligations, You may act only
|
|
169
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
170
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
171
|
+
defend, and hold each Contributor harmless for any liability
|
|
172
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
173
|
+
of your accepting any such warranty or additional liability.
|
|
174
|
+
|
|
175
|
+
END OF TERMS AND CONDITIONS
|
|
176
|
+
|
|
177
|
+
Copyright 2023-2026 Sebastien Rousseau
|
|
178
|
+
|
|
179
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
180
|
+
you may not use this file except in compliance with the License.
|
|
181
|
+
You may obtain a copy of the License at
|
|
182
|
+
|
|
183
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
184
|
+
|
|
185
|
+
Unless required by applicable law or agreed to in writing, software
|
|
186
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
187
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
188
|
+
See the License for the specific language governing permissions and
|
|
189
|
+
limitations under the License.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iso20022-mcp
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Unified Model Context Protocol (MCP) gateway for ISO 20022: one small set of meta-tools (search, describe, validate, generate, parse) routing across the pain, pacs, camt and acmt message families.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: iso-20022,mcp,payments,pain001,pacs008,camt053,acmt001,fintech
|
|
8
|
+
Author: Sebastien Rousseau
|
|
9
|
+
Author-email: sebastian.rousseau@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Provides-Extra: acmt
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Provides-Extra: camt
|
|
21
|
+
Provides-Extra: pacs
|
|
22
|
+
Provides-Extra: pain
|
|
23
|
+
Requires-Dist: acmt001-mcp (>=0.0.5) ; extra == "acmt" or extra == "all"
|
|
24
|
+
Requires-Dist: camt053-mcp (>=0.0.12) ; extra == "camt" or extra == "all"
|
|
25
|
+
Requires-Dist: mcp (>=1.2)
|
|
26
|
+
Requires-Dist: pacs008-mcp (>=0.0.4) ; extra == "pacs" or extra == "all"
|
|
27
|
+
Requires-Dist: pain001-mcp (>=0.0.55) ; extra == "pain" or extra == "all"
|
|
28
|
+
Project-URL: Homepage, https://iso20022-mcp.com
|
|
29
|
+
Project-URL: Repository, https://github.com/sebastienrousseau/iso20022-mcp
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# iso20022-mcp: The Unified MCP Gateway for ISO 20022
|
|
33
|
+
|
|
34
|
+
**One [Model Context Protocol][mcp] server, one small set of meta-tools —
|
|
35
|
+
`search`, `list_families`, `describe`, `validate`, `generate`, `parse` — that
|
|
36
|
+
route across every ISO 20022 message family (`pain` · `pacs` · `camt` ·
|
|
37
|
+
`acmt`).** Install one thing, discover the whole suite: an agent sees a handful
|
|
38
|
+
of verbs instead of the 60+ tools spread across five individual servers.
|
|
39
|
+
|
|
40
|
+
> **Latest release: v0.0.1** — 6 routing meta-tools over stdio, light core
|
|
41
|
+
> (only `mcp`), backing family servers as optional extras, for Python 3.10+.
|
|
42
|
+
> The front door to the [ISO 20022 MCP suite](#the-suite).
|
|
43
|
+
|
|
44
|
+
## Why a gateway
|
|
45
|
+
|
|
46
|
+
The suite has a dedicated, best-in-class server per message family. That depth
|
|
47
|
+
is the point — but an agent shouldn't have to know *which* of five servers and
|
|
48
|
+
*which* of sixty tools it needs before it can act. `iso20022-mcp` is the thin
|
|
49
|
+
routing layer on top: ask it in plain terms ("I need to reconcile a
|
|
50
|
+
statement", "generate a credit transfer"), and it points you at — or executes
|
|
51
|
+
against — the right family. Small tool surface, whole-suite reach.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
The core is light. Add the families you need as extras (or `[all]`):
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
pip install "iso20022-mcp[all]" # every family
|
|
59
|
+
pip install "iso20022-mcp[pacs,camt]" # just interbank + statements
|
|
60
|
+
pip install iso20022-mcp # core only; families report as not installed
|
|
61
|
+
# or run without installing:
|
|
62
|
+
uvx --from "iso20022-mcp[all]" iso20022-mcp
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
MCP client config (e.g. Claude Desktop):
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"iso20022": {
|
|
71
|
+
"command": "iso20022-mcp"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## The meta-tools
|
|
78
|
+
|
|
79
|
+
| Tool | What it does | Families |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `search` | Find message types by use-case / keyword ("reconciliation", "pacs.008"). | all (catalogue) |
|
|
82
|
+
| `list_families` | List families, their capabilities, and which backing packages are installed. | all |
|
|
83
|
+
| `describe` | Required fields + input JSON Schema for a message type. | all |
|
|
84
|
+
| `validate` | Validate records against a message type's schema. | all |
|
|
85
|
+
| `generate` | Generate a validated ISO 20022 XML message from records. | pain · pacs · acmt |
|
|
86
|
+
| `parse` | Parse an inbound ISO 20022 XML message into structured data. | pacs · camt |
|
|
87
|
+
|
|
88
|
+
Operations are capability-aware: `generate` on a `camt.053` statement (an
|
|
89
|
+
inbound format) returns a clear, explanatory error rather than failing
|
|
90
|
+
obscurely, and every "package not installed" case tells you exactly what to
|
|
91
|
+
`pip install`.
|
|
92
|
+
|
|
93
|
+
## Routing
|
|
94
|
+
|
|
95
|
+
| Prefix | Family | Backing server | generate | parse |
|
|
96
|
+
| --- | --- | --- | :---: | :---: |
|
|
97
|
+
| `pain` | Customer Credit Transfer Initiation | [`pain001-mcp`][pain001-mcp] | ✅ | — |
|
|
98
|
+
| `pacs` | FI-to-FI Customer Credit Transfer | [`pacs008-mcp`][pacs008-mcp] | ✅ | ✅ |
|
|
99
|
+
| `camt` | Bank-to-Customer Statement | [`camt053-mcp`][camt053-mcp] | — | ✅ |
|
|
100
|
+
| `acmt` | Account Opening Instruction | [`acmt001-mcp`][acmt001-mcp] | ✅ | — |
|
|
101
|
+
|
|
102
|
+
The gateway imports each backing server **lazily and optionally** — the core
|
|
103
|
+
depends only on `mcp`, and a family's server is loaded on first use. Message
|
|
104
|
+
types are matched on their prefix (`pacs.008.001.08` → `pacs`).
|
|
105
|
+
|
|
106
|
+
## Example
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
search(query="reconciliation")
|
|
110
|
+
→ [{ "message_type": "camt.053", "family": "camt", "package": "camt053-mcp" }]
|
|
111
|
+
|
|
112
|
+
describe(message_type="pacs.008") # required fields + input schema
|
|
113
|
+
validate(message_type="pacs.008", records=[…])
|
|
114
|
+
generate(message_type="pain.001", records=[…]) # → validated XML
|
|
115
|
+
parse(message_type="camt.053", xml="<Document>…")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
For the dedicated reconciliation engine that matches `camt.053` statements
|
|
119
|
+
against expected `pain.001` payments, see [`reconcile-mcp`][reconcile-mcp].
|
|
120
|
+
|
|
121
|
+
## The suite
|
|
122
|
+
|
|
123
|
+
`iso20022-mcp` is the front door to a family of vendor-neutral, Python-native
|
|
124
|
+
ISO 20022 MCP servers:
|
|
125
|
+
|
|
126
|
+
- [`pain001-mcp`][pain001-mcp] — customer credit transfer initiation.
|
|
127
|
+
- [`pacs008-mcp`][pacs008-mcp] — FI-to-FI credit transfers.
|
|
128
|
+
- [`camt053-mcp`][camt053-mcp] — bank-to-customer statements.
|
|
129
|
+
- [`acmt001-mcp`][acmt001-mcp] — account opening instructions.
|
|
130
|
+
- [`reconcile-mcp`][reconcile-mcp] — explainable statement/payment reconciliation.
|
|
131
|
+
- [`bankstatementparser-mcp`][bsp-mcp] — MT94x / BAI2 / OFX parsing.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
git clone https://github.com/sebastienrousseau/iso20022-mcp
|
|
137
|
+
cd iso20022-mcp
|
|
138
|
+
python -m venv .venv && . .venv/bin/activate
|
|
139
|
+
pip install -e . && pip install pytest pytest-cov ruff black mypy
|
|
140
|
+
pytest # 100% branch coverage gate (backends faked)
|
|
141
|
+
ruff check iso20022_mcp tests && black --check iso20022_mcp tests && mypy iso20022_mcp
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Licence
|
|
145
|
+
|
|
146
|
+
Licensed under the [Apache License, Version 2.0](LICENSE).
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
`mcp-name: io.github.sebastienrousseau/iso20022-mcp`
|
|
151
|
+
|
|
152
|
+
[mcp]: https://modelcontextprotocol.io
|
|
153
|
+
[pain001-mcp]: https://github.com/sebastienrousseau/pain001-mcp
|
|
154
|
+
[pacs008-mcp]: https://github.com/sebastienrousseau/pacs008-mcp
|
|
155
|
+
[camt053-mcp]: https://github.com/sebastienrousseau/camt053-mcp
|
|
156
|
+
[acmt001-mcp]: https://github.com/sebastienrousseau/acmt001-mcp
|
|
157
|
+
[reconcile-mcp]: https://github.com/sebastienrousseau/reconcile-mcp
|
|
158
|
+
[bsp-mcp]: https://github.com/sebastienrousseau/bankstatementparser-mcp
|
|
159
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# iso20022-mcp: The Unified MCP Gateway for ISO 20022
|
|
2
|
+
|
|
3
|
+
**One [Model Context Protocol][mcp] server, one small set of meta-tools —
|
|
4
|
+
`search`, `list_families`, `describe`, `validate`, `generate`, `parse` — that
|
|
5
|
+
route across every ISO 20022 message family (`pain` · `pacs` · `camt` ·
|
|
6
|
+
`acmt`).** Install one thing, discover the whole suite: an agent sees a handful
|
|
7
|
+
of verbs instead of the 60+ tools spread across five individual servers.
|
|
8
|
+
|
|
9
|
+
> **Latest release: v0.0.1** — 6 routing meta-tools over stdio, light core
|
|
10
|
+
> (only `mcp`), backing family servers as optional extras, for Python 3.10+.
|
|
11
|
+
> The front door to the [ISO 20022 MCP suite](#the-suite).
|
|
12
|
+
|
|
13
|
+
## Why a gateway
|
|
14
|
+
|
|
15
|
+
The suite has a dedicated, best-in-class server per message family. That depth
|
|
16
|
+
is the point — but an agent shouldn't have to know *which* of five servers and
|
|
17
|
+
*which* of sixty tools it needs before it can act. `iso20022-mcp` is the thin
|
|
18
|
+
routing layer on top: ask it in plain terms ("I need to reconcile a
|
|
19
|
+
statement", "generate a credit transfer"), and it points you at — or executes
|
|
20
|
+
against — the right family. Small tool surface, whole-suite reach.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
The core is light. Add the families you need as extras (or `[all]`):
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install "iso20022-mcp[all]" # every family
|
|
28
|
+
pip install "iso20022-mcp[pacs,camt]" # just interbank + statements
|
|
29
|
+
pip install iso20022-mcp # core only; families report as not installed
|
|
30
|
+
# or run without installing:
|
|
31
|
+
uvx --from "iso20022-mcp[all]" iso20022-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
MCP client config (e.g. Claude Desktop):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"iso20022": {
|
|
40
|
+
"command": "iso20022-mcp"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The meta-tools
|
|
47
|
+
|
|
48
|
+
| Tool | What it does | Families |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `search` | Find message types by use-case / keyword ("reconciliation", "pacs.008"). | all (catalogue) |
|
|
51
|
+
| `list_families` | List families, their capabilities, and which backing packages are installed. | all |
|
|
52
|
+
| `describe` | Required fields + input JSON Schema for a message type. | all |
|
|
53
|
+
| `validate` | Validate records against a message type's schema. | all |
|
|
54
|
+
| `generate` | Generate a validated ISO 20022 XML message from records. | pain · pacs · acmt |
|
|
55
|
+
| `parse` | Parse an inbound ISO 20022 XML message into structured data. | pacs · camt |
|
|
56
|
+
|
|
57
|
+
Operations are capability-aware: `generate` on a `camt.053` statement (an
|
|
58
|
+
inbound format) returns a clear, explanatory error rather than failing
|
|
59
|
+
obscurely, and every "package not installed" case tells you exactly what to
|
|
60
|
+
`pip install`.
|
|
61
|
+
|
|
62
|
+
## Routing
|
|
63
|
+
|
|
64
|
+
| Prefix | Family | Backing server | generate | parse |
|
|
65
|
+
| --- | --- | --- | :---: | :---: |
|
|
66
|
+
| `pain` | Customer Credit Transfer Initiation | [`pain001-mcp`][pain001-mcp] | ✅ | — |
|
|
67
|
+
| `pacs` | FI-to-FI Customer Credit Transfer | [`pacs008-mcp`][pacs008-mcp] | ✅ | ✅ |
|
|
68
|
+
| `camt` | Bank-to-Customer Statement | [`camt053-mcp`][camt053-mcp] | — | ✅ |
|
|
69
|
+
| `acmt` | Account Opening Instruction | [`acmt001-mcp`][acmt001-mcp] | ✅ | — |
|
|
70
|
+
|
|
71
|
+
The gateway imports each backing server **lazily and optionally** — the core
|
|
72
|
+
depends only on `mcp`, and a family's server is loaded on first use. Message
|
|
73
|
+
types are matched on their prefix (`pacs.008.001.08` → `pacs`).
|
|
74
|
+
|
|
75
|
+
## Example
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
search(query="reconciliation")
|
|
79
|
+
→ [{ "message_type": "camt.053", "family": "camt", "package": "camt053-mcp" }]
|
|
80
|
+
|
|
81
|
+
describe(message_type="pacs.008") # required fields + input schema
|
|
82
|
+
validate(message_type="pacs.008", records=[…])
|
|
83
|
+
generate(message_type="pain.001", records=[…]) # → validated XML
|
|
84
|
+
parse(message_type="camt.053", xml="<Document>…")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For the dedicated reconciliation engine that matches `camt.053` statements
|
|
88
|
+
against expected `pain.001` payments, see [`reconcile-mcp`][reconcile-mcp].
|
|
89
|
+
|
|
90
|
+
## The suite
|
|
91
|
+
|
|
92
|
+
`iso20022-mcp` is the front door to a family of vendor-neutral, Python-native
|
|
93
|
+
ISO 20022 MCP servers:
|
|
94
|
+
|
|
95
|
+
- [`pain001-mcp`][pain001-mcp] — customer credit transfer initiation.
|
|
96
|
+
- [`pacs008-mcp`][pacs008-mcp] — FI-to-FI credit transfers.
|
|
97
|
+
- [`camt053-mcp`][camt053-mcp] — bank-to-customer statements.
|
|
98
|
+
- [`acmt001-mcp`][acmt001-mcp] — account opening instructions.
|
|
99
|
+
- [`reconcile-mcp`][reconcile-mcp] — explainable statement/payment reconciliation.
|
|
100
|
+
- [`bankstatementparser-mcp`][bsp-mcp] — MT94x / BAI2 / OFX parsing.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
git clone https://github.com/sebastienrousseau/iso20022-mcp
|
|
106
|
+
cd iso20022-mcp
|
|
107
|
+
python -m venv .venv && . .venv/bin/activate
|
|
108
|
+
pip install -e . && pip install pytest pytest-cov ruff black mypy
|
|
109
|
+
pytest # 100% branch coverage gate (backends faked)
|
|
110
|
+
ruff check iso20022_mcp tests && black --check iso20022_mcp tests && mypy iso20022_mcp
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Licence
|
|
114
|
+
|
|
115
|
+
Licensed under the [Apache License, Version 2.0](LICENSE).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
`mcp-name: io.github.sebastienrousseau/iso20022-mcp`
|
|
120
|
+
|
|
121
|
+
[mcp]: https://modelcontextprotocol.io
|
|
122
|
+
[pain001-mcp]: https://github.com/sebastienrousseau/pain001-mcp
|
|
123
|
+
[pacs008-mcp]: https://github.com/sebastienrousseau/pacs008-mcp
|
|
124
|
+
[camt053-mcp]: https://github.com/sebastienrousseau/camt053-mcp
|
|
125
|
+
[acmt001-mcp]: https://github.com/sebastienrousseau/acmt001-mcp
|
|
126
|
+
[reconcile-mcp]: https://github.com/sebastienrousseau/reconcile-mcp
|
|
127
|
+
[bsp-mcp]: https://github.com/sebastienrousseau/bankstatementparser-mcp
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copyright (C) 2023-2026 Sebastien Rousseau.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
12
|
+
# implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""iso20022-mcp: a unified Model Context Protocol gateway for ISO 20022."""
|
|
17
|
+
|
|
18
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Copyright (C) 2023-2026 Sebastien Rousseau.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
12
|
+
# implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""Family registry and dispatch for the unified ISO 20022 gateway.
|
|
17
|
+
|
|
18
|
+
The gateway presents ONE small set of meta-tools that route across the ISO
|
|
19
|
+
20022 message families -- ``pain`` (initiation), ``pacs`` (interbank), ``camt``
|
|
20
|
+
(statements) and ``acmt`` (account management) -- so an agent sees a handful of
|
|
21
|
+
verbs instead of the 60+ tools spread across the five individual servers.
|
|
22
|
+
|
|
23
|
+
Each family is backed by its dedicated ``-mcp`` server, imported *lazily and
|
|
24
|
+
optionally*: the gateway core depends only on ``mcp``, and a family's server is
|
|
25
|
+
installed on demand (``pip install pacs008-mcp``). Dispatch loads the backing
|
|
26
|
+
module through :func:`_load_module` -- a single indirection point -- and raises
|
|
27
|
+
a clear, actionable :class:`ValueError` if the family is unknown, the backing
|
|
28
|
+
package is not installed, or the requested operation is unsupported for that
|
|
29
|
+
family (e.g. generating a ``camt.053`` statement, which is an inbound format).
|
|
30
|
+
|
|
31
|
+
The backing servers share identical signatures for the core verbs, discovered
|
|
32
|
+
from their source:
|
|
33
|
+
|
|
34
|
+
* ``list_message_types()``
|
|
35
|
+
* ``get_required_fields(message_type)``
|
|
36
|
+
* ``get_input_schema(message_type)``
|
|
37
|
+
* ``validate_records(message_type, records)``
|
|
38
|
+
* ``generate_message(message_type, records)`` (pain / pacs / acmt only)
|
|
39
|
+
* parse: ``parse_message(xml)`` (pacs) / ``parse_statement(xml)`` (camt)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import importlib
|
|
45
|
+
from collections.abc import Callable
|
|
46
|
+
from typing import Any, cast
|
|
47
|
+
|
|
48
|
+
# Family prefix -> backing server and capabilities. ``parse`` names the backing
|
|
49
|
+
# parse function (parse signatures differ per family) or is None when the
|
|
50
|
+
# family exposes no parser through its server.
|
|
51
|
+
FAMILIES: dict[str, dict[str, Any]] = {
|
|
52
|
+
"pain": {
|
|
53
|
+
"family": "pain.001",
|
|
54
|
+
"title": "Customer Credit Transfer Initiation",
|
|
55
|
+
"module": "pain001_mcp.server",
|
|
56
|
+
"package": "pain001-mcp",
|
|
57
|
+
"generate": True,
|
|
58
|
+
"parse": None,
|
|
59
|
+
},
|
|
60
|
+
"pacs": {
|
|
61
|
+
"family": "pacs.008",
|
|
62
|
+
"title": "FI to FI Customer Credit Transfer",
|
|
63
|
+
"module": "pacs008_mcp.server",
|
|
64
|
+
"package": "pacs008-mcp",
|
|
65
|
+
"generate": True,
|
|
66
|
+
"parse": "parse_message",
|
|
67
|
+
},
|
|
68
|
+
"camt": {
|
|
69
|
+
"family": "camt.053",
|
|
70
|
+
"title": "Bank to Customer Statement",
|
|
71
|
+
"module": "camt053_mcp.server",
|
|
72
|
+
"package": "camt053-mcp",
|
|
73
|
+
"generate": False,
|
|
74
|
+
"parse": "parse_statement",
|
|
75
|
+
},
|
|
76
|
+
"acmt": {
|
|
77
|
+
"family": "acmt.001",
|
|
78
|
+
"title": "Account Opening Instruction",
|
|
79
|
+
"module": "acmt001_mcp.server",
|
|
80
|
+
"package": "acmt001-mcp",
|
|
81
|
+
"generate": True,
|
|
82
|
+
"parse": None,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Curated catalogue for keyword/use-case search. Kept small and human: it maps
|
|
87
|
+
# what people *say they want to do* to the right message type and family.
|
|
88
|
+
CATALOG: list[dict[str, Any]] = [
|
|
89
|
+
{
|
|
90
|
+
"message_type": "pain.001",
|
|
91
|
+
"family": "pain",
|
|
92
|
+
"name": "Customer Credit Transfer Initiation",
|
|
93
|
+
"keywords": [
|
|
94
|
+
"payment initiation",
|
|
95
|
+
"credit transfer",
|
|
96
|
+
"make a payment",
|
|
97
|
+
"outgoing payment",
|
|
98
|
+
"pay supplier",
|
|
99
|
+
"salary",
|
|
100
|
+
"payroll",
|
|
101
|
+
"initiate",
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"message_type": "pacs.008",
|
|
106
|
+
"family": "pacs",
|
|
107
|
+
"name": "FI to FI Customer Credit Transfer",
|
|
108
|
+
"keywords": [
|
|
109
|
+
"interbank",
|
|
110
|
+
"settlement",
|
|
111
|
+
"wire",
|
|
112
|
+
"correspondent banking",
|
|
113
|
+
"cbpr+",
|
|
114
|
+
"swift mx",
|
|
115
|
+
"fi to fi",
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"message_type": "camt.053",
|
|
120
|
+
"family": "camt",
|
|
121
|
+
"name": "Bank to Customer Statement",
|
|
122
|
+
"keywords": [
|
|
123
|
+
"statement",
|
|
124
|
+
"end of day",
|
|
125
|
+
"reconciliation",
|
|
126
|
+
"booked entries",
|
|
127
|
+
"closing balance",
|
|
128
|
+
"eod",
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"message_type": "camt.052",
|
|
133
|
+
"family": "camt",
|
|
134
|
+
"name": "Bank to Customer Account Report",
|
|
135
|
+
"keywords": [
|
|
136
|
+
"intraday",
|
|
137
|
+
"account report",
|
|
138
|
+
"provisional",
|
|
139
|
+
"interim balance",
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"message_type": "acmt.001",
|
|
144
|
+
"family": "acmt",
|
|
145
|
+
"name": "Account Opening Instruction",
|
|
146
|
+
"keywords": [
|
|
147
|
+
"account opening",
|
|
148
|
+
"onboarding",
|
|
149
|
+
"open an account",
|
|
150
|
+
"kyc",
|
|
151
|
+
"account management",
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _load_module(module_name: str) -> Any:
|
|
158
|
+
"""Import a backing server module by name.
|
|
159
|
+
|
|
160
|
+
Single indirection point for dispatch, so tests can inject fake family
|
|
161
|
+
modules without installing the real (heavy) servers.
|
|
162
|
+
"""
|
|
163
|
+
return importlib.import_module(module_name)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def family_for(message_type: str) -> dict[str, Any]:
|
|
167
|
+
"""Return the family record for a message type (matched on its prefix).
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
ValueError: if the prefix is not one of the known families.
|
|
171
|
+
"""
|
|
172
|
+
prefix = str(message_type).split(".")[0].strip().lower()
|
|
173
|
+
fam = FAMILIES.get(prefix)
|
|
174
|
+
if fam is None:
|
|
175
|
+
known = ", ".join(sorted(FAMILIES))
|
|
176
|
+
raise ValueError(
|
|
177
|
+
f"unknown ISO 20022 family for {message_type!r}; "
|
|
178
|
+
f"supported prefixes: {known}"
|
|
179
|
+
)
|
|
180
|
+
return fam
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def resolve(message_type: str, func_name: str) -> Callable[..., Any]:
|
|
184
|
+
"""Resolve a backing callable for ``message_type``'s family.
|
|
185
|
+
|
|
186
|
+
Loads the family's server module (installing guidance in the error if it is
|
|
187
|
+
absent) and returns the named function.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ValueError: unknown family, backing package not installed, or the
|
|
191
|
+
function is missing on the backing module.
|
|
192
|
+
"""
|
|
193
|
+
fam = family_for(message_type)
|
|
194
|
+
try:
|
|
195
|
+
module = _load_module(fam["module"])
|
|
196
|
+
except ImportError as exc:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f"{fam['family']} operations need the '{fam['package']}' package. "
|
|
199
|
+
f"Install it with: pip install {fam['package']}"
|
|
200
|
+
) from exc
|
|
201
|
+
func = getattr(module, func_name, None)
|
|
202
|
+
if func is None: # pragma: no cover - defensive; siblings share the API
|
|
203
|
+
raise ValueError(
|
|
204
|
+
f"backing server for {fam['family']} has no {func_name!r}"
|
|
205
|
+
)
|
|
206
|
+
return cast("Callable[..., Any]", func)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def family_summary() -> list[dict[str, Any]]:
|
|
210
|
+
"""Summarise every family: capabilities, backing package, install status."""
|
|
211
|
+
rows: list[dict[str, Any]] = []
|
|
212
|
+
for prefix, fam in FAMILIES.items():
|
|
213
|
+
try:
|
|
214
|
+
_load_module(fam["module"])
|
|
215
|
+
installed = True
|
|
216
|
+
except ImportError:
|
|
217
|
+
installed = False
|
|
218
|
+
rows.append(
|
|
219
|
+
{
|
|
220
|
+
"prefix": prefix,
|
|
221
|
+
"family": fam["family"],
|
|
222
|
+
"title": fam["title"],
|
|
223
|
+
"package": fam["package"],
|
|
224
|
+
"installed": installed,
|
|
225
|
+
"operations": _operations(fam),
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
return rows
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _operations(fam: dict[str, Any]) -> list[str]:
|
|
232
|
+
"""The verbs supported for a family, given its capabilities."""
|
|
233
|
+
ops = ["describe", "validate"]
|
|
234
|
+
if fam["generate"]:
|
|
235
|
+
ops.append("generate")
|
|
236
|
+
if fam["parse"]:
|
|
237
|
+
ops.append("parse")
|
|
238
|
+
return ops
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def search_catalog(query: str) -> list[dict[str, Any]]:
|
|
242
|
+
"""Return catalogue entries matching ``query`` by name, type or keyword.
|
|
243
|
+
|
|
244
|
+
Case-insensitive substring match; empty/whitespace query returns the whole
|
|
245
|
+
catalogue so an agent can browse.
|
|
246
|
+
"""
|
|
247
|
+
q = query.strip().lower()
|
|
248
|
+
results: list[dict[str, Any]] = []
|
|
249
|
+
for entry in CATALOG:
|
|
250
|
+
haystack = " ".join(
|
|
251
|
+
[entry["message_type"], entry["name"], *entry["keywords"]]
|
|
252
|
+
).lower()
|
|
253
|
+
if not q or q in haystack:
|
|
254
|
+
results.append(
|
|
255
|
+
{
|
|
256
|
+
"message_type": entry["message_type"],
|
|
257
|
+
"family": entry["family"],
|
|
258
|
+
"name": entry["name"],
|
|
259
|
+
"package": FAMILIES[entry["family"]]["package"],
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
return results
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Copyright (C) 2023-2026 Sebastien Rousseau.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
12
|
+
# implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""Unified Model Context Protocol (MCP) gateway for ISO 20022.
|
|
17
|
+
|
|
18
|
+
One server, one small set of meta-tools -- ``search``, ``list_families``,
|
|
19
|
+
``describe``, ``validate``, ``generate`` and ``parse`` -- that route across the
|
|
20
|
+
ISO 20022 message families (pain / pacs / camt / acmt) to their dedicated
|
|
21
|
+
backing servers. An agent installs one thing and discovers the whole suite,
|
|
22
|
+
instead of wiring up five servers and choosing between 60+ tools.
|
|
23
|
+
|
|
24
|
+
Each family's backing server is an optional dependency, imported on demand;
|
|
25
|
+
install only the families you need (``pip install iso20022-mcp[all]`` for
|
|
26
|
+
everything). Tools return JSON-serializable data; on a :class:`ValueError`
|
|
27
|
+
(unknown family, backing package missing, or an unsupported operation) they
|
|
28
|
+
return an ``{"error": ...}`` payload rather than raising.
|
|
29
|
+
|
|
30
|
+
Launching the server:
|
|
31
|
+
* As a console script::
|
|
32
|
+
|
|
33
|
+
iso20022-mcp
|
|
34
|
+
|
|
35
|
+
* In an MCP client config (e.g. Claude Desktop)::
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"iso20022": {
|
|
40
|
+
"command": "iso20022-mcp"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
The server communicates over stdio (FastMCP's default transport).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from typing import Annotated, Any
|
|
49
|
+
|
|
50
|
+
from mcp.server.fastmcp import FastMCP
|
|
51
|
+
from mcp.types import ToolAnnotations
|
|
52
|
+
from pydantic import Field
|
|
53
|
+
|
|
54
|
+
from iso20022_mcp import __version__, registry
|
|
55
|
+
|
|
56
|
+
server = FastMCP("iso20022")
|
|
57
|
+
# FastMCP does not expose a version kwarg; without this override the MCP SDK's
|
|
58
|
+
# own version leaks into serverInfo.version, breaking manifest/runtime
|
|
59
|
+
# coherence checks (e.g. Glama scoring).
|
|
60
|
+
server._mcp_server.version = __version__
|
|
61
|
+
|
|
62
|
+
# Every tool routes to a pure, side-effect-free reader on a backing server (or
|
|
63
|
+
# reads the gateway's own catalogue). Nothing opens a caller-supplied path or
|
|
64
|
+
# reaches an external system, so all are marked readOnly + idempotent, never
|
|
65
|
+
# destructive, and closed-world.
|
|
66
|
+
_PURE_READ = ToolAnnotations(
|
|
67
|
+
readOnlyHint=True,
|
|
68
|
+
destructiveHint=False,
|
|
69
|
+
idempotentHint=True,
|
|
70
|
+
openWorldHint=False,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
_MT_DESC = (
|
|
74
|
+
"ISO 20022 message type or family prefix, e.g. 'pacs.008' or 'camt.053'."
|
|
75
|
+
)
|
|
76
|
+
_RECORDS_DESC = (
|
|
77
|
+
"List of record objects to validate or generate a message from."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@server.tool(
|
|
82
|
+
annotations=_PURE_READ,
|
|
83
|
+
description=(
|
|
84
|
+
"Search the ISO 20022 catalogue by use-case, message type or keyword "
|
|
85
|
+
"(e.g. 'reconciliation', 'make a payment', 'pacs.008') and get the "
|
|
86
|
+
"matching message types, their family, and which package provides them."
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
def search(
|
|
90
|
+
query: Annotated[
|
|
91
|
+
str,
|
|
92
|
+
Field(description="Use-case, message type or keyword. Empty = all."),
|
|
93
|
+
] = "",
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
"""Find ISO 20022 message types matching a query."""
|
|
96
|
+
return {"results": registry.search_catalog(query)}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@server.tool(
|
|
100
|
+
annotations=_PURE_READ,
|
|
101
|
+
description=(
|
|
102
|
+
"List every ISO 20022 family the gateway routes to (pain, pacs, camt, "
|
|
103
|
+
"acmt): its capabilities, backing package, and whether that package is "
|
|
104
|
+
"installed in this environment."
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
def list_families() -> dict[str, Any]:
|
|
108
|
+
"""List the supported families and their install status."""
|
|
109
|
+
return {"families": registry.family_summary()}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@server.tool(
|
|
113
|
+
annotations=_PURE_READ,
|
|
114
|
+
description=(
|
|
115
|
+
"Describe a message type: its required fields and input JSON Schema, "
|
|
116
|
+
"resolved from the family's backing server."
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
def describe(
|
|
120
|
+
message_type: Annotated[str, Field(description=_MT_DESC)],
|
|
121
|
+
) -> dict[str, Any]:
|
|
122
|
+
"""Return the required fields and input schema for a message type."""
|
|
123
|
+
try:
|
|
124
|
+
required = registry.resolve(message_type, "get_required_fields")
|
|
125
|
+
schema = registry.resolve(message_type, "get_input_schema")
|
|
126
|
+
return {
|
|
127
|
+
"message_type": message_type,
|
|
128
|
+
"family": registry.family_for(message_type)["family"],
|
|
129
|
+
"required_fields": required(message_type),
|
|
130
|
+
"input_schema": schema(message_type),
|
|
131
|
+
}
|
|
132
|
+
except ValueError as exc:
|
|
133
|
+
return {"error": str(exc)}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@server.tool(
|
|
137
|
+
annotations=_PURE_READ,
|
|
138
|
+
description=(
|
|
139
|
+
"Validate records for a message type against its JSON Schema, via the "
|
|
140
|
+
"family's backing server."
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
def validate(
|
|
144
|
+
message_type: Annotated[str, Field(description=_MT_DESC)],
|
|
145
|
+
records: Annotated[list[dict[str, Any]], Field(description=_RECORDS_DESC)],
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
"""Validate records for a message type."""
|
|
148
|
+
try:
|
|
149
|
+
func = registry.resolve(message_type, "validate_records")
|
|
150
|
+
return func(message_type, records)
|
|
151
|
+
except ValueError as exc:
|
|
152
|
+
return {"error": str(exc)}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@server.tool(
|
|
156
|
+
annotations=_PURE_READ,
|
|
157
|
+
description=(
|
|
158
|
+
"Generate a validated ISO 20022 XML message from records. Supported "
|
|
159
|
+
"for initiation and interbank families (pain, pacs, acmt); statement "
|
|
160
|
+
"families (camt) are inbound-only and return an explanatory error."
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
def generate(
|
|
164
|
+
message_type: Annotated[str, Field(description=_MT_DESC)],
|
|
165
|
+
records: Annotated[list[dict[str, Any]], Field(description=_RECORDS_DESC)],
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Generate an ISO 20022 message from records."""
|
|
168
|
+
try:
|
|
169
|
+
fam = registry.family_for(message_type)
|
|
170
|
+
if not fam["generate"]:
|
|
171
|
+
return {
|
|
172
|
+
"error": (
|
|
173
|
+
f"{fam['family']} is an inbound statement format; generate "
|
|
174
|
+
f"is not supported. Use 'parse' or 'validate' instead."
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
func = registry.resolve(message_type, "generate_message")
|
|
178
|
+
return func(message_type, records)
|
|
179
|
+
except ValueError as exc:
|
|
180
|
+
return {"error": str(exc)}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@server.tool(
|
|
184
|
+
annotations=_PURE_READ,
|
|
185
|
+
description=(
|
|
186
|
+
"Parse an inbound ISO 20022 XML message into structured data. "
|
|
187
|
+
"Supported for interbank (pacs) and statement (camt) families; "
|
|
188
|
+
"initiation families return an explanatory error."
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
def parse(
|
|
192
|
+
message_type: Annotated[str, Field(description=_MT_DESC)],
|
|
193
|
+
xml: Annotated[str, Field(description="Raw ISO 20022 XML to parse.")],
|
|
194
|
+
) -> dict[str, Any]:
|
|
195
|
+
"""Parse an inbound ISO 20022 XML message."""
|
|
196
|
+
try:
|
|
197
|
+
fam = registry.family_for(message_type)
|
|
198
|
+
parse_fn = fam["parse"]
|
|
199
|
+
if not parse_fn:
|
|
200
|
+
return {
|
|
201
|
+
"error": (
|
|
202
|
+
f"{fam['family']} has no inbound parser in this gateway. "
|
|
203
|
+
f"Parsing is available for pacs and camt families."
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
func = registry.resolve(message_type, parse_fn)
|
|
207
|
+
return func(xml)
|
|
208
|
+
except ValueError as exc:
|
|
209
|
+
return {"error": str(exc)}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main() -> None:
|
|
213
|
+
"""Run the ISO 20022 gateway MCP server over stdio (``iso20022-mcp``)."""
|
|
214
|
+
server.run()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
main()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "iso20022-mcp"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Unified Model Context Protocol (MCP) gateway for ISO 20022: one small set of meta-tools (search, describe, validate, generate, parse) routing across the pain, pacs, camt and acmt message families."
|
|
5
|
+
authors = ["Sebastien Rousseau <sebastian.rousseau@gmail.com>"]
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
repository = "https://github.com/sebastienrousseau/iso20022-mcp"
|
|
9
|
+
homepage = "https://iso20022-mcp.com"
|
|
10
|
+
keywords = [
|
|
11
|
+
"iso-20022",
|
|
12
|
+
"mcp",
|
|
13
|
+
"payments",
|
|
14
|
+
"pain001",
|
|
15
|
+
"pacs008",
|
|
16
|
+
"camt053",
|
|
17
|
+
"acmt001",
|
|
18
|
+
"fintech",
|
|
19
|
+
]
|
|
20
|
+
packages = [
|
|
21
|
+
{ include = "iso20022_mcp" },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.poetry.scripts]
|
|
25
|
+
iso20022-mcp = "iso20022_mcp.server:main"
|
|
26
|
+
|
|
27
|
+
[tool.poetry.dependencies]
|
|
28
|
+
python = ">=3.10,<4.0"
|
|
29
|
+
mcp = ">=1.2"
|
|
30
|
+
# Backing family servers are optional: install only the families you need, or
|
|
31
|
+
# `pip install iso20022-mcp[all]` for everything. The gateway imports them
|
|
32
|
+
# lazily and returns an actionable error if a needed family is absent.
|
|
33
|
+
pain001-mcp = { version = ">=0.0.55", optional = true }
|
|
34
|
+
pacs008-mcp = { version = ">=0.0.4", optional = true }
|
|
35
|
+
camt053-mcp = { version = ">=0.0.12", optional = true }
|
|
36
|
+
acmt001-mcp = { version = ">=0.0.5", optional = true }
|
|
37
|
+
|
|
38
|
+
[tool.poetry.extras]
|
|
39
|
+
pain = ["pain001-mcp"]
|
|
40
|
+
pacs = ["pacs008-mcp"]
|
|
41
|
+
camt = ["camt053-mcp"]
|
|
42
|
+
acmt = ["acmt001-mcp"]
|
|
43
|
+
all = ["pain001-mcp", "pacs008-mcp", "camt053-mcp", "acmt001-mcp"]
|
|
44
|
+
|
|
45
|
+
[tool.poetry.group.dev.dependencies]
|
|
46
|
+
pytest = ">=9.0.3,<10.0.0"
|
|
47
|
+
pytest-cov = ">=6.0.0,<8.0.0"
|
|
48
|
+
black = "^26.3.1"
|
|
49
|
+
ruff = "^0.1.0"
|
|
50
|
+
mypy = "^1.11.0"
|
|
51
|
+
bandit = "^1.7.0"
|
|
52
|
+
|
|
53
|
+
[build-system]
|
|
54
|
+
requires = ["poetry-core"]
|
|
55
|
+
build-backend = "poetry.core.masonry.api"
|
|
56
|
+
|
|
57
|
+
[tool.black]
|
|
58
|
+
line-length = 79
|
|
59
|
+
target-version = ['py310']
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
|
63
|
+
python_files = "test_*.py"
|
|
64
|
+
python_classes = "Test*"
|
|
65
|
+
python_functions = "test_*"
|
|
66
|
+
addopts = [
|
|
67
|
+
"-ra",
|
|
68
|
+
"--strict-markers",
|
|
69
|
+
"--tb=short",
|
|
70
|
+
"--cov=iso20022_mcp",
|
|
71
|
+
"--cov-branch",
|
|
72
|
+
"--cov-report=term-missing",
|
|
73
|
+
"--cov-fail-under=100",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.coverage.run]
|
|
77
|
+
branch = true
|
|
78
|
+
source = ["iso20022_mcp"]
|
|
79
|
+
|
|
80
|
+
[tool.coverage.report]
|
|
81
|
+
exclude_also = [
|
|
82
|
+
"if __name__ == .__main__.:",
|
|
83
|
+
"pragma: no cover",
|
|
84
|
+
"@overload",
|
|
85
|
+
]
|
|
86
|
+
fail_under = 100
|
|
87
|
+
show_missing = true
|
|
88
|
+
skip_covered = false
|
|
89
|
+
|
|
90
|
+
[tool.ruff]
|
|
91
|
+
line-length = 79
|
|
92
|
+
target-version = "py310"
|
|
93
|
+
extend-exclude = ["*.md", "tmp_env", ".venv", ".eggs"]
|
|
94
|
+
|
|
95
|
+
[tool.ruff.lint]
|
|
96
|
+
select = ["E", "W", "F", "I", "B", "C4", "UP"]
|
|
97
|
+
ignore = ["E501"]
|
|
98
|
+
|
|
99
|
+
[tool.ruff.lint.per-file-ignores]
|
|
100
|
+
"__init__.py" = ["F401"]
|
|
101
|
+
"tests/conftest.py" = ["E402"]
|
|
102
|
+
|
|
103
|
+
[tool.ruff.format]
|
|
104
|
+
quote-style = "double"
|
|
105
|
+
indent-style = "space"
|
|
106
|
+
skip-magic-trailing-comma = false
|
|
107
|
+
|
|
108
|
+
[tool.mypy]
|
|
109
|
+
python_version = "3.10"
|
|
110
|
+
strict = true
|
|
111
|
+
warn_return_any = true
|
|
112
|
+
warn_unused_configs = true
|
|
113
|
+
disallow_untyped_defs = true
|
|
114
|
+
disallow_any_unimported = false
|
|
115
|
+
no_implicit_optional = true
|
|
116
|
+
warn_redundant_casts = true
|
|
117
|
+
warn_unused_ignores = true
|
|
118
|
+
warn_no_return = true
|
|
119
|
+
check_untyped_defs = true
|
|
120
|
+
strict_equality = true
|
|
121
|
+
exclude = [
|
|
122
|
+
"tmp_env/",
|
|
123
|
+
".venv/",
|
|
124
|
+
".eggs/",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
[[tool.mypy.overrides]]
|
|
128
|
+
module = "mcp.*"
|
|
129
|
+
ignore_missing_imports = true
|
|
130
|
+
|
|
131
|
+
[[tool.mypy.overrides]]
|
|
132
|
+
# Backing family servers are dynamically imported optional dependencies; they
|
|
133
|
+
# may not be present when type-checking the gateway in isolation.
|
|
134
|
+
module = [
|
|
135
|
+
"pain001_mcp.*",
|
|
136
|
+
"pacs008_mcp.*",
|
|
137
|
+
"camt053_mcp.*",
|
|
138
|
+
"acmt001_mcp.*",
|
|
139
|
+
]
|
|
140
|
+
ignore_missing_imports = true
|
|
141
|
+
|
|
142
|
+
[[tool.mypy.overrides]]
|
|
143
|
+
# The meta-tools return bare JSON-shaped data routed from dynamically-resolved
|
|
144
|
+
# backing callables. Relax the two strict checks that trips while keeping the
|
|
145
|
+
# rest of strict mode in force.
|
|
146
|
+
module = "iso20022_mcp.server"
|
|
147
|
+
disallow_any_generics = false
|
|
148
|
+
warn_return_any = false
|
|
149
|
+
|
|
150
|
+
[[tool.mypy.overrides]]
|
|
151
|
+
module = "tests.*"
|
|
152
|
+
disallow_untyped_defs = false
|
|
153
|
+
disallow_incomplete_defs = false
|
|
154
|
+
disallow_untyped_calls = false
|
|
155
|
+
warn_unused_ignores = false
|
|
156
|
+
disable_error_code = ["arg-type", "return-value", "attr-defined", "assignment"]
|