qopanza-mcp 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +145 -0
  3. package/dist/index.js +324 -0
  4. package/package.json +47 -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 2026 Gsente LLC
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,145 @@
1
+ # Qopanza MCP server
2
+
3
+ Lets an AI coding agent — Claude Code, Cursor, Windsurf, Claude Desktop —
4
+ scan an app for exposed secrets and open databases, and then fix what it
5
+ finds.
6
+
7
+ The point is that the person using it never sees this. They type:
8
+
9
+ > check my app is secure before I launch
10
+
11
+ and the agent calls `scan_app_url`, gets structured findings back, calls
12
+ `get_fixes`, and applies the edits. No dashboard, no report to read, no
13
+ signup flow in the middle of their work.
14
+
15
+ ## Why this exists rather than a CLI
16
+
17
+ The audience is someone who built an app on Lovable, Bolt, v0 or Replit.
18
+ They do not have a terminal habit, and a scan *report* is homework — a
19
+ list of things to go and do, in vocabulary they do not have. An agent
20
+ with these tools skips the report entirely and changes the code.
21
+
22
+ That also fixes the hardest part of the product: explaining what row-level
23
+ security is. The agent does not need it explained. It needs a policy and a
24
+ file to put it in, which is what `get_fixes` returns.
25
+
26
+ ## Install
27
+
28
+ Nothing to clone or build. Your agent runs it with `npx`, which fetches
29
+ the published package the first time. Node 18 or newer.
30
+
31
+ **Claude Code:**
32
+
33
+ ```bash
34
+ claude mcp add qopanza -e QOPANZA_API_KEY=qsk_... -- npx -y qopanza-mcp
35
+ ```
36
+
37
+ **Codex** (OpenAI):
38
+
39
+ ```bash
40
+ codex mcp add qopanza --env QOPANZA_API_KEY=qsk_... -- npx -y qopanza-mcp
41
+ ```
42
+
43
+ or by hand in `~/.codex/config.toml`:
44
+
45
+ ```toml
46
+ [mcp_servers.qopanza]
47
+ command = "npx"
48
+ args = ["-y", "qopanza-mcp"]
49
+
50
+ [mcp_servers.qopanza.env]
51
+ QOPANZA_API_KEY = "qsk_..."
52
+ ```
53
+
54
+ **Grok Build** (xAI) — in `~/.grok/config.toml`. Grok expands
55
+ `${QOPANZA_API_KEY}` from your shell at start-up, so the key never has to
56
+ be written into the file. `grok mcp doctor qopanza` checks the connection.
57
+
58
+ ```toml
59
+ [mcp_servers.qopanza]
60
+ command = "npx"
61
+ args = ["-y", "qopanza-mcp"]
62
+ env = { QOPANZA_API_KEY = "${QOPANZA_API_KEY}" }
63
+ ```
64
+
65
+ **Cursor** (`~/.cursor/mcp.json`), **Windsurf** (`~/.codeium/windsurf/mcp_config.json`)
66
+ and **Claude Desktop** (`claude_desktop_config.json`) all take the same
67
+ block:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "qopanza": {
73
+ "command": "npx",
74
+ "args": ["-y", "qopanza-mcp"],
75
+ "env": {
76
+ "QOPANZA_API_KEY": "qsk_..."
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ Then ask your agent something like *"scan https://myapp.com for exposed
84
+ secrets and fix what you find"*.
85
+
86
+ `QOPANZA_API_KEY` comes from **API Keys** in the dashboard; a free
87
+ account is enough for every tool except `get_fixes`. The key lives in
88
+ your agent's local config, never in your project's code. `QOPANZA_BASE_URL`
89
+ defaults to `https://api.qopanza.com/v1`; set it only for a self-hosted
90
+ deployment.
91
+
92
+ ### Lovable, Replit and other cloud agents: the hosted server
93
+
94
+ Agents that run in the vendor's cloud cannot launch `npx`. They connect to
95
+ a server by URL instead, and Qopanza hosts one with the same four tools:
96
+
97
+ ```
98
+ https://api.qopanza.com/mcp
99
+ ```
100
+
101
+ Authenticate with your API key, either as `Authorization: Bearer qsk_...`
102
+ or as an `X-API-Key: qsk_...` header.
103
+
104
+ - **Lovable:** Connectors → **+** → **MCP server**. Server name `Qopanza`,
105
+ the URL above, and choose **Bearer token or API key** with your key.
106
+ - **Replit:** Integrations → MCP Servers → **+ Add MCP server**. Display
107
+ name `Qopanza`, the URL above as **MCP Server URL**, then **Advanced
108
+ settings** → header `X-API-Key` with your key → **Test & save**.
109
+
110
+ ### From source
111
+
112
+ For working on the server itself:
113
+
114
+ ```bash
115
+ cd mcp-server
116
+ npm install
117
+ npm run build
118
+ claude mcp add qopanza -- node "$(pwd)/dist/index.js"
119
+ ```
120
+
121
+ ## Tools
122
+
123
+ | Tool | Plan | What it does |
124
+ |---|---|---|
125
+ | `scan_app_url` | Free | Fetches a deployed site and reads the JavaScript it serves. Anything found is already public. |
126
+ | `scan_app_code` | Free | Scans one file's contents — what an agent calls while editing. |
127
+ | `get_fixes` | **App Security** or above | Turns findings into exact edits, config files and SQL policies. |
128
+ | `scan_crypto` | Free | The quantum-vulnerability scan, for code with its own cryptography. |
129
+
130
+ Every tool needs `QOPANZA_API_KEY`. Without one the server does not fail
131
+ silently: the agent is told the key is missing and where to get it. (The
132
+ no-account scan on qopanza.com/scan is a separate, rate-limited endpoint
133
+ this server does not use.)
134
+
135
+ ## The thing an agent must not get wrong
136
+
137
+ `get_fixes` returns steps with an `automatable` flag. **Key rotation is
138
+ never automatable** — it happens in someone else's console, and this
139
+ server has no access to it.
140
+
141
+ An agent that applies the code edits and reports "fixed" while the leaked
142
+ key is still live has made things worse: the user now believes they are
143
+ safe. The tool descriptions say this explicitly, and every response
144
+ repeats the count of manual steps outstanding, because the model reads
145
+ the response far more carefully than it reads the schema.
package/dist/index.js ADDED
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP server for the Qopanza API.
4
+ *
5
+ * The product thesis in one file: the person who needs this most is not
6
+ * going to read a security report. They are going to ask their coding
7
+ * agent "is my app safe to launch", and the agent needs tools that
8
+ * return something it can act on rather than prose it has to interpret.
9
+ *
10
+ * So every tool here returns two things — a human-readable block the
11
+ * agent can relay, and a structured block it can work from. The
12
+ * descriptions are written for a model deciding whether to call a tool,
13
+ * which is a different audience from a developer reading docs, and they
14
+ * matter more than they look: a tool the model never picks is a tool
15
+ * that does not exist.
16
+ */
17
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
18
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
20
+ const BASE_URL = (process.env.QOPANZA_BASE_URL ?? "https://api.qopanza.com/v1").replace(/\/$/, "");
21
+ const API_KEY = process.env.QOPANZA_API_KEY ?? "";
22
+ class ApiError extends Error {
23
+ status;
24
+ constructor(status, message) {
25
+ super(message);
26
+ this.status = status;
27
+ }
28
+ }
29
+ async function call(path, body) {
30
+ const headers = { "Content-Type": "application/json" };
31
+ if (API_KEY)
32
+ headers["X-API-Key"] = API_KEY;
33
+ const response = await fetch(`${BASE_URL}${path}`, {
34
+ method: "POST",
35
+ headers,
36
+ body: JSON.stringify(body),
37
+ });
38
+ if (!response.ok) {
39
+ let detail = response.statusText;
40
+ try {
41
+ const parsed = (await response.json());
42
+ if (typeof parsed.detail === "string")
43
+ detail = parsed.detail;
44
+ }
45
+ catch {
46
+ // Non-JSON error body; statusText is the best available.
47
+ }
48
+ throw new ApiError(response.status, detail);
49
+ }
50
+ return (await response.json());
51
+ }
52
+ const SEVERITY_ORDER = ["critical", "high", "medium", "low"];
53
+ /**
54
+ * Render findings for the model to read aloud.
55
+ *
56
+ * Deliberately leads with the count and the worst item. A model
57
+ * summarising a long list tends to describe the first thing it read, so
58
+ * the first thing it reads should be the thing that matters.
59
+ */
60
+ function renderScan(scan) {
61
+ if (!scan.reachable) {
62
+ return `Could not reach ${scan.target}.\n${scan.error ?? ""}`.trim();
63
+ }
64
+ const lines = [
65
+ `Scanned ${scan.target} — security score ${scan.score}/100`,
66
+ scan.summary,
67
+ "",
68
+ `Files checked: ${scan.files_scanned.length}`,
69
+ "",
70
+ ];
71
+ if (scan.findings.length === 0) {
72
+ lines.push("No problems found.");
73
+ return lines.join("\n");
74
+ }
75
+ const sorted = [...scan.findings].sort((a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity));
76
+ for (const finding of sorted) {
77
+ const where = finding.line_number > 0
78
+ ? `${finding.location}:${finding.line_number}`
79
+ : finding.location;
80
+ const hedge = finding.confidence === "likely" ? " (needs confirming)" : "";
81
+ lines.push(`[${finding.severity.toUpperCase()}]${hedge} ${finding.title}`);
82
+ lines.push(` where: ${where}`);
83
+ lines.push(` what: ${finding.explanation}`);
84
+ lines.push(` fix: ${finding.remediation}`);
85
+ lines.push("");
86
+ }
87
+ lines.push(scan.fix_hint);
88
+ return lines.join("\n");
89
+ }
90
+ const TOOLS = [
91
+ {
92
+ name: "scan_app_url",
93
+ description: "Scan a deployed website for exposed API keys, database credentials and " +
94
+ "missing security headers, given only its URL. Reads the JavaScript the " +
95
+ "site actually serves, so anything found is ALREADY PUBLIC to every " +
96
+ "visitor. Use this when the user asks whether their app is safe, secure, " +
97
+ "or ready to launch, or mentions deploying. Works on Lovable, Bolt, v0, " +
98
+ "Replit, Vercel and Netlify apps. Free on every plan.",
99
+ inputSchema: {
100
+ type: "object",
101
+ properties: {
102
+ url: {
103
+ type: "string",
104
+ description: "The site's address. A bare domain like myapp.lovable.app is fine.",
105
+ },
106
+ },
107
+ required: ["url"],
108
+ },
109
+ },
110
+ {
111
+ name: "scan_app_code",
112
+ description: "Scan one file's contents for hardcoded secrets (OpenAI, Stripe, AWS, " +
113
+ "Supabase service keys) and database queries that let any visitor read " +
114
+ "other people's rows. Use this while editing a file, or on files you just " +
115
+ "wrote, before the user deploys. Free.",
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: {
119
+ filename: {
120
+ type: "string",
121
+ description: "Path of the file, used to judge whether it ships to the browser.",
122
+ },
123
+ content: { type: "string", description: "The file's contents." },
124
+ },
125
+ required: ["filename", "content"],
126
+ },
127
+ },
128
+ {
129
+ name: "get_fixes",
130
+ description: "Get the exact changes that fix findings from a scan: code edits with " +
131
+ "before/after, config files to create, SQL policies, and the console steps " +
132
+ "a human must do. Pass the findings array from scan_app_url or " +
133
+ "scan_app_code. Requires a paid plan (App Security or above); returns a " +
134
+ "clear message explaining how to upgrade if the account is on Free.\n\n" +
135
+ "IMPORTANT: steps have an `automatable` flag. Steps where it is false — " +
136
+ "rotating a leaked key in the provider's dashboard, enabling RLS in " +
137
+ "Supabase — CANNOT be done for the user. Never tell the user the problem " +
138
+ "is fixed while any of those remain: a key removed from the code but not " +
139
+ "rotated is still live and still public. Apply the automatable edits, then " +
140
+ "list the manual steps and say plainly that they are outstanding.",
141
+ inputSchema: {
142
+ type: "object",
143
+ properties: {
144
+ findings: {
145
+ type: "array",
146
+ description: "The findings array returned by a scan tool, verbatim.",
147
+ items: { type: "object" },
148
+ },
149
+ host: {
150
+ type: "string",
151
+ // Optional on purpose, and deliberately *not* a two-value enum.
152
+ // An agent handed a choice between Vercel and Netlify will pick
153
+ // one — that is what agents do — and the API would then return
154
+ // a `vercel.json` for a Render site. Omitting this returns the
155
+ // header values plus instructions that work anywhere, which is
156
+ // the correct answer when nobody actually knows.
157
+ enum: [
158
+ "vercel",
159
+ "netlify",
160
+ "cloudflare_pages",
161
+ "firebase",
162
+ "render",
163
+ "fly",
164
+ "github_pages",
165
+ "cloudfront",
166
+ "s3",
167
+ "digitalocean",
168
+ "nginx",
169
+ "other",
170
+ ],
171
+ description: "Where the app is deployed. OMIT THIS unless you actually know — " +
172
+ "scan_app_url returns `detected_host`, read off the site's own " +
173
+ "response headers; pass that value through if it is present. Do " +
174
+ "not guess: a config file for the wrong platform is inert, and " +
175
+ "the user will redeploy believing they are fixed. Omitted means " +
176
+ "'unknown', which returns portable instructions instead.",
177
+ },
178
+ },
179
+ required: ["findings"],
180
+ },
181
+ },
182
+ {
183
+ name: "scan_crypto",
184
+ description: "Scan code for quantum-vulnerable cryptography (RSA, ECDSA, ECDH, MD5, " +
185
+ "SHA-1) and report a quantum risk score. Use this when the user asks about " +
186
+ "quantum safety, post-quantum readiness, or crypto compliance. Note: most " +
187
+ "apps built on Lovable/Bolt/v0 have no cryptography of their own, so an " +
188
+ "empty result here is normal and not a problem — prefer scan_app_url for " +
189
+ "'is my app secure'. Free.",
190
+ inputSchema: {
191
+ type: "object",
192
+ properties: {
193
+ filename: { type: "string" },
194
+ content: { type: "string" },
195
+ },
196
+ required: ["filename", "content"],
197
+ },
198
+ },
199
+ ];
200
+ const server = new Server({ name: "qopanza", version: "0.1.0" }, { capabilities: { tools: {} } });
201
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
202
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
203
+ const { name, arguments: args = {} } = request.params;
204
+ try {
205
+ switch (name) {
206
+ case "scan_app_url": {
207
+ const scan = await call("/app-scan/url", { url: args.url });
208
+ return {
209
+ content: [{ type: "text", text: renderScan(scan) }],
210
+ structuredContent: scan,
211
+ };
212
+ }
213
+ case "scan_app_code": {
214
+ const scan = await call("/app-scan/code", {
215
+ filename: args.filename,
216
+ content: args.content,
217
+ });
218
+ return {
219
+ content: [{ type: "text", text: renderScan(scan) }],
220
+ structuredContent: scan,
221
+ };
222
+ }
223
+ case "get_fixes": {
224
+ const plan = await call("/app-scan/fix", { findings: args.findings, host: args.host ?? null });
225
+ const lines = [
226
+ `${plan.total} fix${plan.total === 1 ? "" : "es"} generated.`,
227
+ "",
228
+ ];
229
+ for (const remediation of plan.remediations) {
230
+ lines.push(`## ${remediation.title}`);
231
+ lines.push(remediation.summary);
232
+ lines.push(`Risk if ignored: ${remediation.risk_if_ignored}`);
233
+ for (const step of remediation.steps) {
234
+ const tag = step.automatable ? "[you can do this]" : "[USER MUST DO THIS]";
235
+ lines.push(` ${step.order}. ${tag} ${step.title}`);
236
+ lines.push(` ${step.detail}`);
237
+ if (step.file)
238
+ lines.push(` file: ${step.file}:${step.line ?? ""}`);
239
+ if (step.create_file)
240
+ lines.push(` create: ${step.create_file}`);
241
+ if (step.console_url)
242
+ lines.push(` go to: ${step.console_url}`);
243
+ }
244
+ lines.push("");
245
+ }
246
+ // Repeated at the end because a model summarising a long response
247
+ // weights the last thing it read, and this is the sentence that
248
+ // stops it reporting a half-done fix as finished.
249
+ lines.push(`${plan.manual_steps_required} step(s) require the user and cannot be ` +
250
+ `automated. The problem is NOT fixed until those are done — in ` +
251
+ `particular, a leaked key that has not been rotated is still live.`);
252
+ return {
253
+ content: [{ type: "text", text: lines.join("\n") }],
254
+ structuredContent: plan,
255
+ };
256
+ }
257
+ case "scan_crypto": {
258
+ const scan = await call("/scan/code", {
259
+ filename: args.filename,
260
+ content: args.content,
261
+ });
262
+ return {
263
+ content: [{ type: "text", text: JSON.stringify(scan, null, 2) }],
264
+ structuredContent: scan,
265
+ };
266
+ }
267
+ default:
268
+ return {
269
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
270
+ isError: true,
271
+ };
272
+ }
273
+ }
274
+ catch (error) {
275
+ // 402 is the interesting one: it is not a failure, it is a price. The
276
+ // model should relay it as an option rather than as an error the user
277
+ // needs to debug, so it is phrased that way here.
278
+ if (error instanceof ApiError && error.status === 402) {
279
+ return {
280
+ content: [
281
+ {
282
+ type: "text",
283
+ text: `This needs a paid plan.\n\n${error.message}\n\n` +
284
+ `The scan results you already have are complete — nothing was ` +
285
+ `hidden. You can fix the problems by hand from the descriptions, ` +
286
+ `or upgrade to have the exact changes generated.`,
287
+ },
288
+ ],
289
+ };
290
+ }
291
+ if (error instanceof ApiError && error.status === 401) {
292
+ return {
293
+ content: [
294
+ {
295
+ type: "text",
296
+ text: "No valid API key. Set QOPANZA_API_KEY in this MCP server's " +
297
+ "environment. A free key takes about a minute to create.",
298
+ },
299
+ ],
300
+ isError: true,
301
+ };
302
+ }
303
+ return {
304
+ content: [
305
+ {
306
+ type: "text",
307
+ text: `Scan failed: ${error instanceof Error ? error.message : String(error)}`,
308
+ },
309
+ ],
310
+ isError: true,
311
+ };
312
+ }
313
+ });
314
+ async function main() {
315
+ // stdio, so the agent launches this as a subprocess. Nothing may be
316
+ // written to stdout except protocol frames — a stray console.log
317
+ // corrupts the stream and the server appears to hang.
318
+ await server.connect(new StdioServerTransport());
319
+ console.error(`Qopanza MCP server ready (api: ${BASE_URL})`);
320
+ }
321
+ main().catch((error) => {
322
+ console.error("Fatal:", error);
323
+ process.exit(1);
324
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "qopanza-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for AI coding agents: scan an app for exposed secrets and open databases, then fix them",
5
+ "license": "Apache-2.0",
6
+ "author": "Gsente LLC",
7
+ "type": "module",
8
+ "bin": { "qopanza-mcp": "dist/index.js" },
9
+ "files": ["dist/index.js", "README.md", "LICENSE"],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "typecheck": "tsc --noEmit",
13
+ "start": "node dist/index.js",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.0.0",
21
+ "typescript": "^5.6.3"
22
+ },
23
+ "engines": { "node": ">=18" },
24
+ "homepage": "https://qopanza.com/docs#mcp",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/isingomajoel2023/qopanza-sdks.git",
28
+ "directory": "mcp-server"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/isingomajoel2023/qopanza-sdks/issues"
32
+ },
33
+ "keywords": [
34
+ "mcp",
35
+ "model-context-protocol",
36
+ "claude",
37
+ "cursor",
38
+ "security",
39
+ "secrets",
40
+ "supabase",
41
+ "post-quantum",
42
+ "qopanza"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }