@dnsdoctor/mcp 1.2.0
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.
- package/LICENSE +202 -0
- package/README.md +169 -0
- package/dist/api.d.ts +57 -0
- package/dist/api.js +178 -0
- package/dist/api.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +127 -0
- package/dist/index.js.map +1 -0
- package/dist/tools.d.ts +25 -0
- package/dist/tools.js +52 -0
- package/dist/tools.js.map +1 -0
- package/instructions.txt +1 -0
- package/package.json +50 -0
- package/tools.json +354 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# DNS Doctor — Claude Code plugin
|
|
2
|
+
|
|
3
|
+
Diagnose and fix a domain's email authentication (SPF, DMARC, DKIM, MX, blacklist,
|
|
4
|
+
domain/SSL expiry) from inside Claude. This plugin bundles the DNS Doctor **skill**
|
|
5
|
+
(the scan → diagnose → fix workflow) and an **MCP server config** pointing at the
|
|
6
|
+
hosted DNS Doctor tools.
|
|
7
|
+
|
|
8
|
+
The moat: every fix record you get back is generated and validated by a
|
|
9
|
+
deterministic engine — RFC grammar plus the SPF 10-lookup counter — **never an LLM
|
|
10
|
+
guess**. Your agent hands the human a record that already parses correctly, not a
|
|
11
|
+
plausible-looking string that silently fails.
|
|
12
|
+
|
|
13
|
+
## What's inside
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
claude-plugin/
|
|
17
|
+
├── .claude-plugin/plugin.json # plugin manifest
|
|
18
|
+
├── .mcp.json # MCP server: https://dnsdoctor.dev/mcp (HTTP)
|
|
19
|
+
├── skills/dns-doctor/SKILL.md # the scan → diagnose → fix workflow
|
|
20
|
+
├── src/ # @dnsdoctor/mcp — the local stdio MCP server
|
|
21
|
+
├── tools.json # the 11 tool definitions (generated, never hand-edited)
|
|
22
|
+
├── instructions.txt # the server's own `initialize` guidance (generated)
|
|
23
|
+
├── tests/ # vitest suite for the stdio server
|
|
24
|
+
├── package.json tsconfig.json # npm package + build
|
|
25
|
+
├── LICENSE # Apache-2.0
|
|
26
|
+
└── README.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Tools it adds
|
|
30
|
+
|
|
31
|
+
| Tool | Does |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `scan_domain` | Fresh scan of a domain; full report. |
|
|
34
|
+
| `get_report` | Persisted report (scans once if none exists). |
|
|
35
|
+
| `build_dmarc_upgrade` | A validated DMARC enforcement record — `p=reject` only when the server-derived alignment gate passes. |
|
|
36
|
+
| `count_spf_lookups` | The SPF DNS-lookup count against the RFC limit of 10. |
|
|
37
|
+
| `validate_dmarc_record` | Parse and validate a DMARC record, tag by tag. |
|
|
38
|
+
| `generate_dmarc_record` | Build a DMARC record from a policy + reporting address. |
|
|
39
|
+
| `check_dkim_selector` | Look up one DKIM selector and check the key. |
|
|
40
|
+
| `parse_dmarc_report` | Parse an aggregate (RUA) report file into rows. |
|
|
41
|
+
| `check_record` | Read any DNS record type for a name. |
|
|
42
|
+
| `check_reverse_dns` | PTR / forward-confirmed reverse DNS for an IP. |
|
|
43
|
+
| `start_monitoring_signup` | A sign-up link to hand to the human who owns the domain. Sends no email and creates nothing — they open it, sign in on our page themselves (a social provider or an emailed link, whichever that deployment offers), and the domain is carried over to their dashboard already filled in; monitoring starts once they verify it with a TXT record. |
|
|
44
|
+
|
|
45
|
+
Over the hosted HTTP transport the `dnsdoctor://domains` resource (your
|
|
46
|
+
monitored domains) is always listed; reading it needs an API token and is
|
|
47
|
+
refused without one. The local stdio server registers the tools only — no
|
|
48
|
+
resource. Anonymous access is enough for a one-off diagnosis either way.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
### Claude Code
|
|
53
|
+
|
|
54
|
+
Add the marketplace/repo and enable the plugin:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
/plugin marketplace add dnsdoctor/claude-plugin
|
|
58
|
+
/plugin install dns-doctor
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> Public home: [github.com/dnsdoctor/claude-plugin](https://github.com/dnsdoctor/claude-plugin)
|
|
62
|
+
> (org `dnsdoctor`, domain-verified). The plugin is developed in the DNS Doctor
|
|
63
|
+
> monorepo and published here as clean release snapshots.
|
|
64
|
+
|
|
65
|
+
Or point Claude Code at a local checkout of this directory during development.
|
|
66
|
+
Once enabled, the skill auto-loads and the `dns-doctor` MCP server connects to
|
|
67
|
+
`https://dnsdoctor.dev/mcp`.
|
|
68
|
+
|
|
69
|
+
### claude.ai (MCP connector)
|
|
70
|
+
|
|
71
|
+
Add a custom connector with:
|
|
72
|
+
|
|
73
|
+
- **URL:** `https://dnsdoctor.dev/mcp`
|
|
74
|
+
- **Transport:** Streamable HTTP
|
|
75
|
+
- **Auth:** none (anonymous) — or a Bearer token (below)
|
|
76
|
+
|
|
77
|
+
## Optional: API token for monitored domains
|
|
78
|
+
|
|
79
|
+
Anonymous access covers scanning and fixes. A per-account API token unlocks the
|
|
80
|
+
`dnsdoctor://domains` resource (your continuously-monitored domains and their
|
|
81
|
+
latest per-check statuses).
|
|
82
|
+
|
|
83
|
+
1. Sign in at <https://dnsdoctor.dev> → **Settings → API tokens** → create a token.
|
|
84
|
+
The plaintext (`dnsd_…`) is shown once; copy it.
|
|
85
|
+
2. Add the `Authorization` header to the server in `.mcp.json`:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"dns-doctor": {
|
|
91
|
+
"type": "http",
|
|
92
|
+
"url": "https://dnsdoctor.dev/mcp",
|
|
93
|
+
"headers": { "Authorization": "Bearer ${DNSDOCTOR_API_TOKEN}" }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Then export `DNSDOCTOR_API_TOKEN=dnsd_YOUR_TOKEN` in your environment. Never
|
|
100
|
+
commit the token.
|
|
101
|
+
|
|
102
|
+
### Transport
|
|
103
|
+
|
|
104
|
+
Two supported public transports, same 11 tools:
|
|
105
|
+
|
|
106
|
+
- **Hosted streamable HTTP** — `https://dnsdoctor.dev/mcp`, wired in this
|
|
107
|
+
plugin's `.mcp.json`. No install, no keys.
|
|
108
|
+
- **Local stdio** — `npx -y @dnsdoctor/mcp`. A thin server that runs on your
|
|
109
|
+
machine and calls the public DNS Doctor REST API; it holds no diagnosis logic
|
|
110
|
+
of its own and relays every record string verbatim.
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"mcpServers": {
|
|
115
|
+
"dns-doctor": {
|
|
116
|
+
"command": "npx",
|
|
117
|
+
"args": ["-y", "@dnsdoctor/mcp"],
|
|
118
|
+
"env": { "DNSDOCTOR_API_TOKEN": "dnsd_YOUR_TOKEN" }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`DNSDOCTOR_API_TOKEN` is optional (anonymous access covers scanning and fixes).
|
|
125
|
+
`DNSDOCTOR_API_BASE` overrides the origin — it must be an origin that serves the
|
|
126
|
+
`/api/…` paths, i.e. the public site rather than a bare backend port.
|
|
127
|
+
|
|
128
|
+
### Build the stdio server from source
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npm ci
|
|
132
|
+
npm test
|
|
133
|
+
npm run build # -> dist/index.js, the package's bin entrypoint
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`tools.json` and `instructions.txt` are **generated from the hosted server** and
|
|
137
|
+
pinned by a backend test — never hand-edit them, and never add a tool
|
|
138
|
+
description to `src/`. Both files ship in the npm tarball; without them the
|
|
139
|
+
client has no tools and none of its safety guidance.
|
|
140
|
+
|
|
141
|
+
## Worked example
|
|
142
|
+
|
|
143
|
+
> **You:** Our invoices keep landing in customers' spam. Domain is `example.com`.
|
|
144
|
+
>
|
|
145
|
+
> **Claude** (`scan_domain` → `example.com`): DMARC is at `p=none`, so nothing
|
|
146
|
+
> enforces alignment — receivers can't tell your mail from a spoof. SPF passes with
|
|
147
|
+
> 7 of 10 lookups used. DKIM: a selector was found and is valid. Not blacklisted.
|
|
148
|
+
> Domain expires in 240 days.
|
|
149
|
+
>
|
|
150
|
+
> **Claude** (`build_dmarc_upgrade` → `example.com`): SPF is aligned and DKIM is
|
|
151
|
+
> present, so enforcement to `p=reject` is safe. Publish this exact TXT record at
|
|
152
|
+
> `_dmarc.example.com` — **paste it verbatim, don't edit it**:
|
|
153
|
+
>
|
|
154
|
+
> ```
|
|
155
|
+
> v=DMARC1; p=reject; rua=mailto:dmarc@example.com; adkim=r; aspf=r
|
|
156
|
+
> ```
|
|
157
|
+
>
|
|
158
|
+
> Apply it in your DNS host once you've confirmed it's approved, then ask me to
|
|
159
|
+
> re-scan to verify. Want the domain watched continuously with aggregate-report
|
|
160
|
+
> alerts? I can send a setup link to your email.
|
|
161
|
+
|
|
162
|
+
## Learn more
|
|
163
|
+
|
|
164
|
+
- **Methodology** (how the verdicts are computed): <https://dnsdoctor.dev/methodology>
|
|
165
|
+
- **REST API / OpenAPI schema:** <https://dnsdoctor.dev/api/v1/openapi.json>
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
Apache-2.0 — see [LICENSE](./LICENSE).
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE HTTP layer of the thin client.
|
|
3
|
+
*
|
|
4
|
+
* Every tool handler goes through here, and nothing else in `src/` may hold a
|
|
5
|
+
* base URL or invent an error message. Two rules are load-bearing:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Relay, never compose.** Response bodies are returned untouched — the
|
|
8
|
+
* records and signup URLs in them are authored server-side by the fixengine
|
|
9
|
+
* and are the product's trust moat. This file never edits a payload.
|
|
10
|
+
* 2. **A transport failure is never a verdict.** 429/5xx/network faults map to
|
|
11
|
+
* explicitly transient errors that tell the agent to retry rather than
|
|
12
|
+
* report a diagnosis, mirroring the backend's `temperror` discipline.
|
|
13
|
+
*
|
|
14
|
+
* A 422 relays the API's `detail` **verbatim** — those strings are a wire
|
|
15
|
+
* contract shared with the hosted MCP server; rewording them here would let the
|
|
16
|
+
* two surfaces disagree about the same malformed input.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEFAULT_API_BASE = "https://dnsdoctor.dev";
|
|
19
|
+
/** Shared tail of every transient message — one string, so all of them agree. */
|
|
20
|
+
export declare const TRANSIENT_SUFFIX = "retry; never report this as a verdict";
|
|
21
|
+
export declare const RATE_LIMITED_MESSAGE = "rate limited \u2014 slow down and retry";
|
|
22
|
+
/** Error raised for any non-2xx response or transport fault. */
|
|
23
|
+
export declare class ApiError extends Error {
|
|
24
|
+
readonly status: number | null;
|
|
25
|
+
readonly transient: boolean;
|
|
26
|
+
constructor(message: string, status: number | null, transient: boolean);
|
|
27
|
+
}
|
|
28
|
+
/** Base URL for the DNS Doctor API; `DNSDOCTOR_API_BASE` overrides for local runs. */
|
|
29
|
+
export declare function apiBase(): string;
|
|
30
|
+
/** This package's version — also what the server reports in `initialize`. */
|
|
31
|
+
export declare function packageVersion(): string;
|
|
32
|
+
/**
|
|
33
|
+
* The UA every call carries — this client's only attribution.
|
|
34
|
+
*
|
|
35
|
+
* `api/agents.py::agent_family` is an ALLOWLIST: an unrecognized UA is not
|
|
36
|
+
* counted at all, so without this marker the npm channel would be invisible in
|
|
37
|
+
* `dnsdoctor_agent_requests_total`, which is the number the agent channel's
|
|
38
|
+
* kill criterion reads. The matching entry lives in that module's `_FAMILIES`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function userAgent(): string;
|
|
41
|
+
/** GET a JSON resource. `path` is API-root-relative and already encoded. */
|
|
42
|
+
export declare function getJson(path: string): Promise<unknown>;
|
|
43
|
+
/** POST a JSON body and return the parsed response, untouched. */
|
|
44
|
+
export declare function postJson(path: string, body: unknown): Promise<unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* Decode base64, rejecting input Node would silently mangle.
|
|
47
|
+
*
|
|
48
|
+
* Not transient: the caller sent bad bytes, so retrying the same call changes
|
|
49
|
+
* nothing — the agent must re-encode instead of reporting a verdict.
|
|
50
|
+
*/
|
|
51
|
+
export declare function decodeBase64(value: string): Buffer;
|
|
52
|
+
/**
|
|
53
|
+
* POST one file as `multipart/form-data` — the shape `/api/tools/dmarc-report-parse`
|
|
54
|
+
* expects. The tool takes the report as base64, so the bytes are decoded here and
|
|
55
|
+
* uploaded as a file part rather than re-encoded into JSON.
|
|
56
|
+
*/
|
|
57
|
+
export declare function postFile(path: string, field: string, filename: string, contentBase64: string): Promise<unknown>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE HTTP layer of the thin client.
|
|
3
|
+
*
|
|
4
|
+
* Every tool handler goes through here, and nothing else in `src/` may hold a
|
|
5
|
+
* base URL or invent an error message. Two rules are load-bearing:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Relay, never compose.** Response bodies are returned untouched — the
|
|
8
|
+
* records and signup URLs in them are authored server-side by the fixengine
|
|
9
|
+
* and are the product's trust moat. This file never edits a payload.
|
|
10
|
+
* 2. **A transport failure is never a verdict.** 429/5xx/network faults map to
|
|
11
|
+
* explicitly transient errors that tell the agent to retry rather than
|
|
12
|
+
* report a diagnosis, mirroring the backend's `temperror` discipline.
|
|
13
|
+
*
|
|
14
|
+
* A 422 relays the API's `detail` **verbatim** — those strings are a wire
|
|
15
|
+
* contract shared with the hosted MCP server; rewording them here would let the
|
|
16
|
+
* two surfaces disagree about the same malformed input.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
export const DEFAULT_API_BASE = "https://dnsdoctor.dev";
|
|
20
|
+
/** Shared tail of every transient message — one string, so all of them agree. */
|
|
21
|
+
export const TRANSIENT_SUFFIX = "retry; never report this as a verdict";
|
|
22
|
+
export const RATE_LIMITED_MESSAGE = "rate limited — slow down and retry";
|
|
23
|
+
/** Error raised for any non-2xx response or transport fault. */
|
|
24
|
+
export class ApiError extends Error {
|
|
25
|
+
status;
|
|
26
|
+
transient;
|
|
27
|
+
constructor(message, status, transient) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "ApiError";
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.transient = transient;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Base URL for the DNS Doctor API; `DNSDOCTOR_API_BASE` overrides for local runs. */
|
|
35
|
+
export function apiBase() {
|
|
36
|
+
const raw = process.env.DNSDOCTOR_API_BASE?.trim();
|
|
37
|
+
const base = raw ? raw : DEFAULT_API_BASE;
|
|
38
|
+
return base.replace(/\/+$/, "");
|
|
39
|
+
}
|
|
40
|
+
/** Optional bearer token — anonymous access works, a token only raises the budget. */
|
|
41
|
+
function authHeaders() {
|
|
42
|
+
const token = process.env.DNSDOCTOR_API_TOKEN?.trim();
|
|
43
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
44
|
+
}
|
|
45
|
+
/** This package's version — also what the server reports in `initialize`. */
|
|
46
|
+
export function packageVersion() {
|
|
47
|
+
try {
|
|
48
|
+
const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
49
|
+
const version = JSON.parse(raw).version;
|
|
50
|
+
return typeof version === "string" ? version : "0.0.0";
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return "0.0.0";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The UA every call carries — this client's only attribution.
|
|
58
|
+
*
|
|
59
|
+
* `api/agents.py::agent_family` is an ALLOWLIST: an unrecognized UA is not
|
|
60
|
+
* counted at all, so without this marker the npm channel would be invisible in
|
|
61
|
+
* `dnsdoctor_agent_requests_total`, which is the number the agent channel's
|
|
62
|
+
* kill criterion reads. The matching entry lives in that module's `_FAMILIES`.
|
|
63
|
+
*/
|
|
64
|
+
export function userAgent() {
|
|
65
|
+
return `dnsdoctor-mcp/${packageVersion()} (+https://dnsdoctor.dev)`;
|
|
66
|
+
}
|
|
67
|
+
/** Pull the API's `detail` out of an error body, preserving its exact text. */
|
|
68
|
+
function detailOf(body) {
|
|
69
|
+
if (typeof body !== "object" || body === null)
|
|
70
|
+
return null;
|
|
71
|
+
const detail = body.detail;
|
|
72
|
+
if (typeof detail === "string")
|
|
73
|
+
return detail;
|
|
74
|
+
if (detail === undefined || detail === null)
|
|
75
|
+
return null;
|
|
76
|
+
return JSON.stringify(detail);
|
|
77
|
+
}
|
|
78
|
+
async function readBody(response) {
|
|
79
|
+
try {
|
|
80
|
+
return await response.json();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function toApiError(response) {
|
|
87
|
+
const detail = detailOf(await readBody(response));
|
|
88
|
+
const status = response.status;
|
|
89
|
+
if (status === 429) {
|
|
90
|
+
// The constant leads, so the agent always reads the same instruction; the
|
|
91
|
+
// API's own detail (which domain, which budget) rides along verbatim.
|
|
92
|
+
const message = detail ? `${RATE_LIMITED_MESSAGE} (${detail})` : RATE_LIMITED_MESSAGE;
|
|
93
|
+
return new ApiError(message, status, true);
|
|
94
|
+
}
|
|
95
|
+
if (status === 503) {
|
|
96
|
+
return new ApiError(`DNS Doctor could not complete the lookup (transient) — ${TRANSIENT_SUFFIX}`, status, true);
|
|
97
|
+
}
|
|
98
|
+
if (status >= 500) {
|
|
99
|
+
return new ApiError(`DNS Doctor returned a server error (HTTP ${status}) — ${TRANSIENT_SUFFIX}`, status, true);
|
|
100
|
+
}
|
|
101
|
+
// 4xx — the API's own `detail` is the answer (422 malformed input, opaque 404,
|
|
102
|
+
// 413 oversize report). Verbatim, always.
|
|
103
|
+
return new ApiError(detail ?? `DNS Doctor request failed (HTTP ${status})`, status, false);
|
|
104
|
+
}
|
|
105
|
+
async function send(path, init) {
|
|
106
|
+
const url = `${apiBase()}${path}`;
|
|
107
|
+
let response;
|
|
108
|
+
try {
|
|
109
|
+
response = await fetch(url, {
|
|
110
|
+
...init,
|
|
111
|
+
headers: {
|
|
112
|
+
Accept: "application/json",
|
|
113
|
+
"User-Agent": userAgent(),
|
|
114
|
+
...authHeaders(),
|
|
115
|
+
...(init.headers ?? {}),
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
121
|
+
throw new ApiError(`Could not reach the DNS Doctor API (${cause}) — ${TRANSIENT_SUFFIX}`, null, true);
|
|
122
|
+
}
|
|
123
|
+
if (!response.ok)
|
|
124
|
+
throw await toApiError(response);
|
|
125
|
+
const body = await readBody(response);
|
|
126
|
+
if (body === null) {
|
|
127
|
+
throw new ApiError(`DNS Doctor returned an unreadable response (HTTP ${response.status}) — ${TRANSIENT_SUFFIX}`, response.status, true);
|
|
128
|
+
}
|
|
129
|
+
return body;
|
|
130
|
+
}
|
|
131
|
+
/** GET a JSON resource. `path` is API-root-relative and already encoded. */
|
|
132
|
+
export function getJson(path) {
|
|
133
|
+
return send(path, { method: "GET" });
|
|
134
|
+
}
|
|
135
|
+
/** POST a JSON body and return the parsed response, untouched. */
|
|
136
|
+
export function postJson(path, body) {
|
|
137
|
+
return send(path, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: { "Content-Type": "application/json" },
|
|
140
|
+
body: JSON.stringify(body),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Decode base64, rejecting input Node would silently mangle.
|
|
145
|
+
*
|
|
146
|
+
* Not transient: the caller sent bad bytes, so retrying the same call changes
|
|
147
|
+
* nothing — the agent must re-encode instead of reporting a verdict.
|
|
148
|
+
*/
|
|
149
|
+
export function decodeBase64(value) {
|
|
150
|
+
// Whitespace (MIME line breaks) and the base64url alphabet are both accepted
|
|
151
|
+
// — Node decodes them and so should we; the round-trip below is what rejects
|
|
152
|
+
// input that is not base64 at all.
|
|
153
|
+
const compact = value.replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/");
|
|
154
|
+
const bytes = Buffer.from(compact, "base64");
|
|
155
|
+
if (bytes.toString("base64").replace(/=+$/, "") !== compact.replace(/=+$/, "")) {
|
|
156
|
+
throw new ApiError("'content_base64' is not valid base64 — re-encode the report file and try again", null, false);
|
|
157
|
+
}
|
|
158
|
+
return bytes;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* POST one file as `multipart/form-data` — the shape `/api/tools/dmarc-report-parse`
|
|
162
|
+
* expects. The tool takes the report as base64, so the bytes are decoded here and
|
|
163
|
+
* uploaded as a file part rather than re-encoded into JSON.
|
|
164
|
+
*/
|
|
165
|
+
// `async`, so a decode rejection reaches callers the same way a transport one
|
|
166
|
+
// does — a synchronous throw from a Promise-returning function is a trap.
|
|
167
|
+
export async function postFile(path, field, filename, contentBase64) {
|
|
168
|
+
// Validated, not try/caught: `Buffer.from(s, "base64")` never throws — it
|
|
169
|
+
// silently DISCARDS characters outside the alphabet. Uploading those bytes
|
|
170
|
+
// would earn a 422 "not a DMARC report file", telling the agent its report is
|
|
171
|
+
// invalid when the real fault is the encoding it sent us.
|
|
172
|
+
const bytes = decodeBase64(contentBase64);
|
|
173
|
+
const form = new FormData();
|
|
174
|
+
form.append(field, new Blob([bytes], { type: "application/octet-stream" }), filename);
|
|
175
|
+
// No Content-Type header: fetch sets it with the multipart boundary.
|
|
176
|
+
return send(path, { method: "POST", body: form });
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `@dnsdoctor/mcp` — a thin stdio MCP server in front of the public DNS Doctor
|
|
4
|
+
* REST API.
|
|
5
|
+
*
|
|
6
|
+
* It holds no diagnosis logic of its own. Every tool is one HTTP call whose
|
|
7
|
+
* response is relayed **verbatim**: the record strings, signup URLs and verdicts
|
|
8
|
+
* in those payloads are authored server-side by the fixengine, and rewriting one
|
|
9
|
+
* here would break the product's central invariant (an LLM — or a client — never
|
|
10
|
+
* composes an authoritative record). The tool definitions come from `tools.json`
|
|
11
|
+
* (see `tools.ts`), so the only thing this file decides is which endpoint each
|
|
12
|
+
* tool calls.
|
|
13
|
+
*/
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
type ToolArgs = Record<string, unknown>;
|
|
16
|
+
/** Run one tool and return the API's parsed body, untouched. */
|
|
17
|
+
export declare function callTool(name: string, args?: ToolArgs): Promise<unknown>;
|
|
18
|
+
/** Build the MCP server with the fixture's tools and instructions registered. */
|
|
19
|
+
export declare function createServer(): Server;
|
|
20
|
+
/** Every fixture tool has a route — a missing one would 'unknown tool' at runtime. */
|
|
21
|
+
export declare function unroutedTools(): string[];
|
|
22
|
+
export declare function main(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* True when this module is the process entrypoint — false when a test imports it.
|
|
25
|
+
*
|
|
26
|
+
* Both sides are resolved through `realpathSync` because npm installs `bin`
|
|
27
|
+
* entries as **symlinks**: launched as `dnsdoctor-mcp` (or via `npx`),
|
|
28
|
+
* `process.argv[1]` is `node_modules/.bin/dnsdoctor-mcp` while
|
|
29
|
+
* `import.meta.url` is already the symlink's target, so a raw string compare is
|
|
30
|
+
* false for every installed user and the server silently never starts. That is
|
|
31
|
+
* invisible to `node dist/index.js` and shows up only in the `npm pack` smoke.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isEntrypoint(): boolean;
|
|
34
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `@dnsdoctor/mcp` — a thin stdio MCP server in front of the public DNS Doctor
|
|
4
|
+
* REST API.
|
|
5
|
+
*
|
|
6
|
+
* It holds no diagnosis logic of its own. Every tool is one HTTP call whose
|
|
7
|
+
* response is relayed **verbatim**: the record strings, signup URLs and verdicts
|
|
8
|
+
* in those payloads are authored server-side by the fixengine, and rewriting one
|
|
9
|
+
* here would break the product's central invariant (an LLM — or a client — never
|
|
10
|
+
* composes an authoritative record). The tool definitions come from `tools.json`
|
|
11
|
+
* (see `tools.ts`), so the only thing this file decides is which endpoint each
|
|
12
|
+
* tool calls.
|
|
13
|
+
*/
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
import { realpathSync } from "node:fs";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { ApiError, getJson, packageVersion, postFile, postJson } from "./api.js";
|
|
20
|
+
import { instructions, listTools, toolNames } from "./tools.js";
|
|
21
|
+
const ROUTES = {
|
|
22
|
+
scan_domain: { kind: "json", path: "/api/v1/scan" },
|
|
23
|
+
get_report: { kind: "report" },
|
|
24
|
+
build_dmarc_upgrade: { kind: "json", path: "/api/v1/dmarc-upgrade" },
|
|
25
|
+
start_monitoring_signup: { kind: "json", path: "/api/v1/signup-url" },
|
|
26
|
+
count_spf_lookups: { kind: "json", path: "/api/tools/spf-count" },
|
|
27
|
+
validate_dmarc_record: { kind: "json", path: "/api/tools/dmarc-validate" },
|
|
28
|
+
generate_dmarc_record: { kind: "json", path: "/api/tools/dmarc-generate" },
|
|
29
|
+
check_dkim_selector: { kind: "json", path: "/api/tools/dkim-check" },
|
|
30
|
+
parse_dmarc_report: {
|
|
31
|
+
kind: "upload",
|
|
32
|
+
path: "/api/tools/dmarc-report-parse",
|
|
33
|
+
field: "file",
|
|
34
|
+
},
|
|
35
|
+
check_record: { kind: "json", path: "/api/tools/check-record" },
|
|
36
|
+
check_reverse_dns: { kind: "json", path: "/api/tools/reverse-dns-check" },
|
|
37
|
+
};
|
|
38
|
+
/** Default upload name when the caller supplies none — the API only reads bytes. */
|
|
39
|
+
const DEFAULT_REPORT_FILENAME = "report.xml";
|
|
40
|
+
function requireString(args, key) {
|
|
41
|
+
const value = args[key];
|
|
42
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
43
|
+
throw new ApiError(`'${key}' is required and must be a non-empty string`, null, false);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
/** Run one tool and return the API's parsed body, untouched. */
|
|
48
|
+
export async function callTool(name, args = {}) {
|
|
49
|
+
const route = ROUTES[name];
|
|
50
|
+
if (route === undefined) {
|
|
51
|
+
throw new ApiError(`unknown tool '${name}'`, null, false);
|
|
52
|
+
}
|
|
53
|
+
if (route.kind === "report") {
|
|
54
|
+
const domain = requireString(args, "domain");
|
|
55
|
+
return getJson(`/api/v1/report/${encodeURIComponent(domain)}`);
|
|
56
|
+
}
|
|
57
|
+
if (route.kind === "upload") {
|
|
58
|
+
const content = requireString(args, "content_base64");
|
|
59
|
+
const filename = args["filename"];
|
|
60
|
+
return postFile(route.path, route.field, typeof filename === "string" && filename.trim() !== "" ? filename : DEFAULT_REPORT_FILENAME, content);
|
|
61
|
+
}
|
|
62
|
+
return postJson(route.path, args);
|
|
63
|
+
}
|
|
64
|
+
function okResult(body) {
|
|
65
|
+
return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
|
|
66
|
+
}
|
|
67
|
+
function errorResult(error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
70
|
+
}
|
|
71
|
+
/** Build the MCP server with the fixture's tools and instructions registered. */
|
|
72
|
+
export function createServer() {
|
|
73
|
+
const server = new Server({ name: "dns-doctor", version: packageVersion() },
|
|
74
|
+
// `instructions` is the hosted server's own preamble, shipped in the pinned
|
|
75
|
+
// fixture: without it a host other than Claude Code (which reads the bundled
|
|
76
|
+
// SKILL.md) gets the tools with none of the safety contract around them.
|
|
77
|
+
{ capabilities: { tools: {} }, instructions: instructions() });
|
|
78
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: listTools() }));
|
|
79
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
80
|
+
const { name, arguments: args } = request.params;
|
|
81
|
+
try {
|
|
82
|
+
return okResult(await callTool(name, args ?? {}));
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return errorResult(error);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return server;
|
|
89
|
+
}
|
|
90
|
+
/** Every fixture tool has a route — a missing one would 'unknown tool' at runtime. */
|
|
91
|
+
export function unroutedTools() {
|
|
92
|
+
return toolNames().filter((name) => ROUTES[name] === undefined);
|
|
93
|
+
}
|
|
94
|
+
export async function main() {
|
|
95
|
+
await createServer().connect(new StdioServerTransport());
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* True when this module is the process entrypoint — false when a test imports it.
|
|
99
|
+
*
|
|
100
|
+
* Both sides are resolved through `realpathSync` because npm installs `bin`
|
|
101
|
+
* entries as **symlinks**: launched as `dnsdoctor-mcp` (or via `npx`),
|
|
102
|
+
* `process.argv[1]` is `node_modules/.bin/dnsdoctor-mcp` while
|
|
103
|
+
* `import.meta.url` is already the symlink's target, so a raw string compare is
|
|
104
|
+
* false for every installed user and the server silently never starts. That is
|
|
105
|
+
* invisible to `node dist/index.js` and shows up only in the `npm pack` smoke.
|
|
106
|
+
*/
|
|
107
|
+
export function isEntrypoint() {
|
|
108
|
+
const argv1 = process.argv[1];
|
|
109
|
+
if (argv1 === undefined)
|
|
110
|
+
return false;
|
|
111
|
+
const resolve = (path) => {
|
|
112
|
+
try {
|
|
113
|
+
return realpathSync(path);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return path;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
return resolve(argv1) === resolve(fileURLToPath(import.meta.url));
|
|
120
|
+
}
|
|
121
|
+
if (isEntrypoint()) {
|
|
122
|
+
main().catch((error) => {
|
|
123
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool definitions and the `initialize` preamble — read from the fixtures,
|
|
3
|
+
* never authored here.
|
|
4
|
+
*
|
|
5
|
+
* `tools.json` and `instructions.txt` are generated from the hosted server's own
|
|
6
|
+
* `list_tools()` and instructions, and pinned byte-for-byte by
|
|
7
|
+
* `backend/tests/mcp_server/test_tools_fixture.py`. That is the whole point: a
|
|
8
|
+
* name, description, schema or instruction sentence edited on the server fails a
|
|
9
|
+
* backend test until the fixtures are regenerated, so this client cannot drift
|
|
10
|
+
* from the surface it claims to mirror. Consequently **no description string,
|
|
11
|
+
* schema, tool name or guidance sentence is written in `src/`** — everything
|
|
12
|
+
* comes from the files.
|
|
13
|
+
*/
|
|
14
|
+
/** One entry of `tools.json`; the shape the MCP `tools/list` result expects. */
|
|
15
|
+
export interface ToolDefinition {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: Record<string, unknown>;
|
|
19
|
+
annotations?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
/** Every tool the hosted server exposes, in the fixture's own (sorted) order. */
|
|
22
|
+
export declare function listTools(): ToolDefinition[];
|
|
23
|
+
/** The tool names, for the dispatch table's coverage check. */
|
|
24
|
+
export declare function toolNames(): string[];
|
|
25
|
+
export declare function instructions(): string;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool definitions and the `initialize` preamble — read from the fixtures,
|
|
3
|
+
* never authored here.
|
|
4
|
+
*
|
|
5
|
+
* `tools.json` and `instructions.txt` are generated from the hosted server's own
|
|
6
|
+
* `list_tools()` and instructions, and pinned byte-for-byte by
|
|
7
|
+
* `backend/tests/mcp_server/test_tools_fixture.py`. That is the whole point: a
|
|
8
|
+
* name, description, schema or instruction sentence edited on the server fails a
|
|
9
|
+
* backend test until the fixtures are regenerated, so this client cannot drift
|
|
10
|
+
* from the surface it claims to mirror. Consequently **no description string,
|
|
11
|
+
* schema, tool name or guidance sentence is written in `src/`** — everything
|
|
12
|
+
* comes from the files.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
/**
|
|
16
|
+
* `tools.json` sits at the package root — one level up from both `src/` (tests,
|
|
17
|
+
* run from source) and `dist/` (the published `bin`), so the same relative URL
|
|
18
|
+
* resolves in either. It ships in `package.json#files`; without it the client
|
|
19
|
+
* has no tools at all, which is why a missing file throws rather than degrading
|
|
20
|
+
* to an empty list.
|
|
21
|
+
*/
|
|
22
|
+
const FIXTURE_URL = new URL("../tools.json", import.meta.url);
|
|
23
|
+
function readFixture() {
|
|
24
|
+
const parsed = JSON.parse(readFileSync(FIXTURE_URL, "utf8"));
|
|
25
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
26
|
+
throw new Error("tools.json is not a non-empty array of tool definitions");
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
const TOOLS = readFixture();
|
|
31
|
+
/** Every tool the hosted server exposes, in the fixture's own (sorted) order. */
|
|
32
|
+
export function listTools() {
|
|
33
|
+
return TOOLS;
|
|
34
|
+
}
|
|
35
|
+
/** The tool names, for the dispatch table's coverage check. */
|
|
36
|
+
export function toolNames() {
|
|
37
|
+
return TOOLS.map((tool) => tool.name);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The in-band guidance the hosted server sends in `initialize`, shipped verbatim.
|
|
41
|
+
*
|
|
42
|
+
* Claude Code users read the same rules from the bundled SKILL.md; every OTHER
|
|
43
|
+
* host running `npx -y @dnsdoctor/mcp` reads them only here. Dropping them would
|
|
44
|
+
* leave an agent with no `temperror`-is-not-a-failure rule, no
|
|
45
|
+
* `not_registered`-is-not-health rule, no records-verbatim rule and no
|
|
46
|
+
* SPF-is-diagnose-only rule — the product's safety contract, not decoration.
|
|
47
|
+
*/
|
|
48
|
+
const INSTRUCTIONS = readFileSync(new URL("../instructions.txt", import.meta.url), "utf8").trim();
|
|
49
|
+
export function instructions() {
|
|
50
|
+
return INSTRUCTIONS;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAUvC;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE9D,SAAS,WAAW;IAClB,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IACtE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,MAA0B,CAAC;AACpC,CAAC;AAED,MAAM,KAAK,GAAqB,WAAW,EAAE,CAAC;AAE9C,iFAAiF;AACjF,MAAM,UAAU,SAAS;IACvB,OAAO,KAAK,CAAC;AACf,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,SAAS;IACvB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,YAAY,GAAW,YAAY,CACvC,IAAI,GAAG,CAAC,qBAAqB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAC/C,MAAM,CACP,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,UAAU,YAAY;IAC1B,OAAO,YAAY,CAAC;AACtB,CAAC"}
|
package/instructions.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DNS Doctor scans a domain's email-authentication posture (SPF, DMARC, DKIM, MX, DNS, blacklist, expiry) and returns deterministic verdicts plus copy-paste fix records generated by a validating engine — never a guessed record. Workflow: call scan_domain (or get_report), read the verdicts failing-first (a 'temperror' status is transient, NOT a failure), then call build_dmarc_upgrade for a DMARC enforcement record. ONE EXCEPTION TO READING FAILING-FIRST: when a report carries not_registered: true the domain has no DNS records at all, so no check ran and every status is an 'info' placeholder — zero failing checks there does NOT mean the domain is healthy. Report that the domain does not resolve (usually a typo), propose no records for it, and do not offer monitoring until it resolves. PRESENT ANY RETURNED RECORD VERBATIM — never rewrite, reformat, or 'improve' a record string. build_dmarc_upgrade may return record: null (the domain does not exist; the DMARC lookup itself hit NXDOMAIN while the existence probe did not resolve; the DMARC lookup temp-failed; or the domain already applies a policy at least as strong as the one this scan justifies) — relay its 'rationale' as the answer and NEVER compose a record yourself to fill the gap. 'policy' describes the returned record and is null whenever record is; the domain's observed policy is in 'current_policy'. A human must approve every DNS change; nothing is applied automatically. SPF is diagnose-only: relay the report's SPF findings, but never propose SPF record edits of your own (e.g. tightening ~all to -all) — an SPF change can silently de-authorize a real sender, which is why the engine emits no SPF fix record. Scan responses end with a next_steps block: relay it — safe DMARC enforcement needs ~30 days of aggregate-report (RUA) evidence that no chat session can watch, so when a domain lacks reporting, call start_monitoring_signup and give the human the signup_url it returns. That tool sends no email and creates nothing: the human opens the link, signs in themselves, and adds the domain themselves. Never ask a human for their email address to pass to us, and never invent one — hand over the link and let them sign in on our page. Also share the report_url and monitor_url links from next_steps.
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dnsdoctor/mcp",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Thin stdio MCP server for DNS Doctor — diagnoses SPF, DMARC, DKIM, MX, DNS, blacklist and expiry over the public DNS Doctor REST API.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "DNS Doctor",
|
|
8
|
+
"url": "https://dnsdoctor.dev"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://dnsdoctor.dev/methodology",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/dnsdoctor/claude-plugin.git"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"dns",
|
|
18
|
+
"email",
|
|
19
|
+
"spf",
|
|
20
|
+
"dmarc",
|
|
21
|
+
"dkim",
|
|
22
|
+
"deliverability"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"dnsdoctor-mcp": "dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"tools.json",
|
|
31
|
+
"instructions.txt"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"prepack": "npm run build",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"typecheck": "tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.21.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.15.0",
|
|
47
|
+
"typescript": "^5.9.2",
|
|
48
|
+
"vitest": "^3.2.4"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/tools.json
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"annotations": {
|
|
4
|
+
"destructiveHint": false,
|
|
5
|
+
"idempotentHint": false,
|
|
6
|
+
"openWorldHint": true,
|
|
7
|
+
"readOnlyHint": false
|
|
8
|
+
},
|
|
9
|
+
"description": "Return a validated DMARC enforcement-upgrade record for a domain. The alignment gate (whether p=reject is safe) is derived server-side from a scan \u2014 a caller can never assert alignment. `record` is null when there is no honest upgrade to offer (the domain does not exist; the DMARC lookup itself hit NXDOMAIN while the existence probe did not resolve; the DMARC lookup temp-failed; or the domain already applies a policy at least as strong as this scan justifies): relay `rationale` and never compose a record to fill the gap. Present a returned record verbatim; a human must approve before publishing.",
|
|
10
|
+
"inputSchema": {
|
|
11
|
+
"properties": {
|
|
12
|
+
"domain": {
|
|
13
|
+
"title": "Domain",
|
|
14
|
+
"type": "string"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"required": [
|
|
18
|
+
"domain"
|
|
19
|
+
],
|
|
20
|
+
"title": "build_dmarc_upgradeArguments",
|
|
21
|
+
"type": "object"
|
|
22
|
+
},
|
|
23
|
+
"name": "build_dmarc_upgrade"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"annotations": {
|
|
27
|
+
"destructiveHint": false,
|
|
28
|
+
"idempotentHint": true,
|
|
29
|
+
"openWorldHint": true,
|
|
30
|
+
"readOnlyHint": true
|
|
31
|
+
},
|
|
32
|
+
"description": "Check ONE specific DKIM selector on a domain \u2014 the exact selector the sending platform uses (e.g. `google`, `s1`), which a full scan's common-selector sweep may miss. Returns the verdict, its explanation, and the published key record when one resolves. No fix record is returned: a DKIM key is generated by the sending platform, so the fix is always to publish what that platform gives the owner.",
|
|
33
|
+
"inputSchema": {
|
|
34
|
+
"properties": {
|
|
35
|
+
"domain": {
|
|
36
|
+
"title": "Domain",
|
|
37
|
+
"type": "string"
|
|
38
|
+
},
|
|
39
|
+
"selector": {
|
|
40
|
+
"title": "Selector",
|
|
41
|
+
"type": "string"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"required": [
|
|
45
|
+
"domain",
|
|
46
|
+
"selector"
|
|
47
|
+
],
|
|
48
|
+
"title": "check_dkim_selectorArguments",
|
|
49
|
+
"type": "object"
|
|
50
|
+
},
|
|
51
|
+
"name": "check_dkim_selector"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"annotations": {
|
|
55
|
+
"destructiveHint": false,
|
|
56
|
+
"idempotentHint": true,
|
|
57
|
+
"openWorldHint": true,
|
|
58
|
+
"readOnlyHint": true
|
|
59
|
+
},
|
|
60
|
+
"description": "Check whether a DNS change has landed: reads the record from the domain's OWN nameservers (cache-free) and from two public caching resolvers, and reports whether they agree. `kind` is one of spf|dmarc|txt|mx|cname|a|aaaa \u2014 pass the kind, not a query name: `dmarc` reads TXT at _dmarc.<domain> and `spf` reads apex TXT, each filtered to the matching record. `host` prepends a label (txt, cname, a and aaaa only). Empty values mean the record is genuinely absent. When in_sync is false, max_wait_seconds is the largest remaining cached TTL \u2014 the wait before those resolvers refresh. This samples two resolvers, so never describe it as worldwide or as propagation coverage.",
|
|
61
|
+
"inputSchema": {
|
|
62
|
+
"properties": {
|
|
63
|
+
"domain": {
|
|
64
|
+
"title": "Domain",
|
|
65
|
+
"type": "string"
|
|
66
|
+
},
|
|
67
|
+
"host": {
|
|
68
|
+
"anyOf": [
|
|
69
|
+
{
|
|
70
|
+
"type": "string"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"type": "null"
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"default": null,
|
|
77
|
+
"title": "Host"
|
|
78
|
+
},
|
|
79
|
+
"kind": {
|
|
80
|
+
"enum": [
|
|
81
|
+
"spf",
|
|
82
|
+
"dmarc",
|
|
83
|
+
"txt",
|
|
84
|
+
"mx",
|
|
85
|
+
"cname",
|
|
86
|
+
"a",
|
|
87
|
+
"aaaa"
|
|
88
|
+
],
|
|
89
|
+
"title": "Kind",
|
|
90
|
+
"type": "string"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"required": [
|
|
94
|
+
"domain",
|
|
95
|
+
"kind"
|
|
96
|
+
],
|
|
97
|
+
"title": "check_recordArguments",
|
|
98
|
+
"type": "object"
|
|
99
|
+
},
|
|
100
|
+
"name": "check_record"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"annotations": {
|
|
104
|
+
"destructiveHint": false,
|
|
105
|
+
"idempotentHint": true,
|
|
106
|
+
"openWorldHint": true,
|
|
107
|
+
"readOnlyHint": true
|
|
108
|
+
},
|
|
109
|
+
"description": "Check one sending IP's forward-confirmed reverse DNS (FCrDNS): reads the IP's PTR record, then resolves that hostname back and reports whether it returns to the same IP. `verdict` is confirmed (the pair agrees \u2014 what receivers want to see), ptr_missing (the IP publishes no reverse record), or mismatch (a PTR that does not resolve back). A PTR on its own proves nothing, because the IP's operator writes its own reverse zone \u2014 only the forward confirmation is evidence, so never report a bare PTR as verified. The fix is always made by whoever controls the IP (the hosting or mail provider), never in the sending domain's own DNS. Pass a public IPv4 or IPv6 address.",
|
|
110
|
+
"inputSchema": {
|
|
111
|
+
"properties": {
|
|
112
|
+
"ip": {
|
|
113
|
+
"title": "Ip",
|
|
114
|
+
"type": "string"
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"required": [
|
|
118
|
+
"ip"
|
|
119
|
+
],
|
|
120
|
+
"title": "check_reverse_dnsArguments",
|
|
121
|
+
"type": "object"
|
|
122
|
+
},
|
|
123
|
+
"name": "check_reverse_dns"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"annotations": {
|
|
127
|
+
"destructiveHint": false,
|
|
128
|
+
"idempotentHint": true,
|
|
129
|
+
"openWorldHint": true,
|
|
130
|
+
"readOnlyHint": true
|
|
131
|
+
},
|
|
132
|
+
"description": "Validate an SPF record and count what it costs. Returns `record_valid` (the record parses as RFC 7208 SPF), `findings` (per-term diagnostics), `has_pass_all` (a `+all` that authorizes the whole internet to send as this domain), `multiple_all` (more than one `all`, which makes everything after the first unreachable), the parsed `terms`, and the lookup count against the limit of 10 with `over_limit`/`near_limit` and the `offending_mechanisms` that push it over. Pass EXACTLY ONE of `domain` (resolves the published record and counts recursively through nested includes) or `record` (parses a pasted record, its own terms only). This is the SPF validator \u2014 there is no separate one. Diagnose-only: no SPF fix record is ever returned, because removing a mechanism can silently de-authorize a real sender \u2014 relay the findings and let the domain's owner decide what to drop.",
|
|
133
|
+
"inputSchema": {
|
|
134
|
+
"properties": {
|
|
135
|
+
"domain": {
|
|
136
|
+
"anyOf": [
|
|
137
|
+
{
|
|
138
|
+
"type": "string"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"type": "null"
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"default": null,
|
|
145
|
+
"title": "Domain"
|
|
146
|
+
},
|
|
147
|
+
"record": {
|
|
148
|
+
"anyOf": [
|
|
149
|
+
{
|
|
150
|
+
"type": "string"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"type": "null"
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
"default": null,
|
|
157
|
+
"title": "Record"
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"title": "count_spf_lookupsArguments",
|
|
161
|
+
"type": "object"
|
|
162
|
+
},
|
|
163
|
+
"name": "count_spf_lookups"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"annotations": {
|
|
167
|
+
"destructiveHint": false,
|
|
168
|
+
"idempotentHint": true,
|
|
169
|
+
"openWorldHint": false,
|
|
170
|
+
"readOnlyHint": true
|
|
171
|
+
},
|
|
172
|
+
"description": "Build a DMARC record from scratch for a domain that has none, using a validating engine \u2014 never compose one yourself. `policy` is none|quarantine|reject; optional `rua_email` (aggregate-report destination), `subdomain_policy`, and `strict_alignment`. The generated record is re-validated before it is returned. Present it verbatim; a human must approve before publishing.",
|
|
173
|
+
"inputSchema": {
|
|
174
|
+
"properties": {
|
|
175
|
+
"policy": {
|
|
176
|
+
"enum": [
|
|
177
|
+
"none",
|
|
178
|
+
"quarantine",
|
|
179
|
+
"reject"
|
|
180
|
+
],
|
|
181
|
+
"title": "Policy",
|
|
182
|
+
"type": "string"
|
|
183
|
+
},
|
|
184
|
+
"rua_email": {
|
|
185
|
+
"anyOf": [
|
|
186
|
+
{
|
|
187
|
+
"type": "string"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"type": "null"
|
|
191
|
+
}
|
|
192
|
+
],
|
|
193
|
+
"default": null,
|
|
194
|
+
"title": "Rua Email"
|
|
195
|
+
},
|
|
196
|
+
"strict_alignment": {
|
|
197
|
+
"default": false,
|
|
198
|
+
"title": "Strict Alignment",
|
|
199
|
+
"type": "boolean"
|
|
200
|
+
},
|
|
201
|
+
"subdomain_policy": {
|
|
202
|
+
"anyOf": [
|
|
203
|
+
{
|
|
204
|
+
"enum": [
|
|
205
|
+
"none",
|
|
206
|
+
"quarantine",
|
|
207
|
+
"reject"
|
|
208
|
+
],
|
|
209
|
+
"type": "string"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"type": "null"
|
|
213
|
+
}
|
|
214
|
+
],
|
|
215
|
+
"default": null,
|
|
216
|
+
"title": "Subdomain Policy"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"required": [
|
|
220
|
+
"policy"
|
|
221
|
+
],
|
|
222
|
+
"title": "generate_dmarc_recordArguments",
|
|
223
|
+
"type": "object"
|
|
224
|
+
},
|
|
225
|
+
"name": "generate_dmarc_record"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"annotations": {
|
|
229
|
+
"destructiveHint": false,
|
|
230
|
+
"idempotentHint": true,
|
|
231
|
+
"openWorldHint": true,
|
|
232
|
+
"readOnlyHint": false
|
|
233
|
+
},
|
|
234
|
+
"description": "Return the stored report for a domain, scanning once only if none exists yet \u2014 the cheap read, and the right default for a first look. Returns the same seven-check report as scan_domain (SPF, DKIM, DMARC, MX, DNS hardening, domain/TLS expiry, blacklist; each with a status, the observed record and any fixengine fix_record), including `scanned_at` so you can judge staleness yourself. Prefer scan_domain when you specifically need state re-read right now \u2014 for example after a DNS change.",
|
|
235
|
+
"inputSchema": {
|
|
236
|
+
"properties": {
|
|
237
|
+
"domain": {
|
|
238
|
+
"title": "Domain",
|
|
239
|
+
"type": "string"
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
"required": [
|
|
243
|
+
"domain"
|
|
244
|
+
],
|
|
245
|
+
"title": "get_reportArguments",
|
|
246
|
+
"type": "object"
|
|
247
|
+
},
|
|
248
|
+
"name": "get_report"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"annotations": {
|
|
252
|
+
"destructiveHint": false,
|
|
253
|
+
"idempotentHint": true,
|
|
254
|
+
"openWorldHint": false,
|
|
255
|
+
"readOnlyHint": true
|
|
256
|
+
},
|
|
257
|
+
"description": "Parse ONE DMARC aggregate (RUA) report into readable per-source aggregates: who sent mail as the domain, how much, and what share was SPF/DKIM aligned. Pass the file's bytes base64-encoded in `content_base64` (XML, .gz or .zip; up to 2 MiB decoded) with an optional `filename`. Nothing is stored \u2014 the report is parsed and discarded.",
|
|
258
|
+
"inputSchema": {
|
|
259
|
+
"properties": {
|
|
260
|
+
"content_base64": {
|
|
261
|
+
"title": "Content Base64",
|
|
262
|
+
"type": "string"
|
|
263
|
+
},
|
|
264
|
+
"filename": {
|
|
265
|
+
"anyOf": [
|
|
266
|
+
{
|
|
267
|
+
"type": "string"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"type": "null"
|
|
271
|
+
}
|
|
272
|
+
],
|
|
273
|
+
"default": null,
|
|
274
|
+
"title": "Filename"
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
"required": [
|
|
278
|
+
"content_base64"
|
|
279
|
+
],
|
|
280
|
+
"title": "parse_dmarc_reportArguments",
|
|
281
|
+
"type": "object"
|
|
282
|
+
},
|
|
283
|
+
"name": "parse_dmarc_report"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"annotations": {
|
|
287
|
+
"destructiveHint": false,
|
|
288
|
+
"idempotentHint": false,
|
|
289
|
+
"openWorldHint": true,
|
|
290
|
+
"readOnlyHint": false
|
|
291
|
+
},
|
|
292
|
+
"description": "Force a fresh scan of a domain and return its full report: seven deterministic checks \u2014 SPF, DKIM, DMARC, MX, DNS hardening, domain/TLS expiry and blacklist listing \u2014 each with a status (pass/warn/fail/info/temperror), the observed record, and a fixengine-generated fix_record where one exists. Explanations are cache-first. A repeat scan of the same domain within a minute reuses the stored report. Use this when you need current state; use get_report when a recent cached answer will do.",
|
|
293
|
+
"inputSchema": {
|
|
294
|
+
"properties": {
|
|
295
|
+
"domain": {
|
|
296
|
+
"title": "Domain",
|
|
297
|
+
"type": "string"
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
"required": [
|
|
301
|
+
"domain"
|
|
302
|
+
],
|
|
303
|
+
"title": "scan_domainArguments",
|
|
304
|
+
"type": "object"
|
|
305
|
+
},
|
|
306
|
+
"name": "scan_domain"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"annotations": {
|
|
310
|
+
"destructiveHint": false,
|
|
311
|
+
"idempotentHint": true,
|
|
312
|
+
"openWorldHint": false,
|
|
313
|
+
"readOnlyHint": true
|
|
314
|
+
},
|
|
315
|
+
"description": "Return a sign-up link to give to the HUMAN who owns a domain, so they can start monitoring it themselves. Nothing is created and no email is sent by this call: the returned signup_url opens a page that explains monitoring and asks them to sign in with their own email address; the domain is carried over to their dashboard, already filled in, only after that, and monitoring itself starts once they prove ownership with a TXT record. Hand them the URL and relay `message` \u2014 never sign in on their behalf.",
|
|
316
|
+
"inputSchema": {
|
|
317
|
+
"properties": {
|
|
318
|
+
"domain": {
|
|
319
|
+
"title": "Domain",
|
|
320
|
+
"type": "string"
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
"required": [
|
|
324
|
+
"domain"
|
|
325
|
+
],
|
|
326
|
+
"title": "start_monitoring_signupArguments",
|
|
327
|
+
"type": "object"
|
|
328
|
+
},
|
|
329
|
+
"name": "start_monitoring_signup"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"annotations": {
|
|
333
|
+
"destructiveHint": false,
|
|
334
|
+
"idempotentHint": true,
|
|
335
|
+
"openWorldHint": false,
|
|
336
|
+
"readOnlyHint": true
|
|
337
|
+
},
|
|
338
|
+
"description": "Validate a pasted DMARC record: parsed tags, level'd findings, and whether it is valid. No DNS lookup \u2014 pass the record string itself. `upgrade_record` previews a stronger policy and is capped at p=quarantine: a pasted record carries no alignment evidence, so p=reject needs a full scan (use build_dmarc_upgrade). Present any returned record verbatim.",
|
|
339
|
+
"inputSchema": {
|
|
340
|
+
"properties": {
|
|
341
|
+
"record": {
|
|
342
|
+
"title": "Record",
|
|
343
|
+
"type": "string"
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
"required": [
|
|
347
|
+
"record"
|
|
348
|
+
],
|
|
349
|
+
"title": "validate_dmarc_recordArguments",
|
|
350
|
+
"type": "object"
|
|
351
|
+
},
|
|
352
|
+
"name": "validate_dmarc_record"
|
|
353
|
+
}
|
|
354
|
+
]
|