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.
@@ -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"]