opentestmcp 0.3.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.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * App user auth (JWT) — tools that call the FastAPI backend at OPENTEST_BACKEND /auth/*.
3
+ * Separate from opentest_login (device flow + ot_live_ API key). No API key is required
4
+ * for these tools; for JWT-protected routes, pass the access_token from login/verify.
5
+ */
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ /**
8
+ * Register MCP tools for routes/auth (email, OTP, password, JWT).
9
+ * Proxies to the same process as opentest-local (OPENTEST_BACKEND, e.g. http://127.0.0.1:8000).
10
+ */
11
+ export declare function registerUserAuthFlowTools(server: McpServer, backendUrl: string): void;
@@ -0,0 +1,251 @@
1
+ import { z } from "zod";
2
+ function toolError(message, code = "backend_error", hints) {
3
+ const payload = { error: message, error_code: code };
4
+ if (hints)
5
+ payload._agent_hints = hints;
6
+ return {
7
+ content: [{ type: "text", text: JSON.stringify(payload) }],
8
+ isError: true,
9
+ };
10
+ }
11
+ function jsonOk(obj) {
12
+ return { content: [{ type: "text", text: JSON.stringify(obj) }] };
13
+ }
14
+ function normalizeBase(backendUrl) {
15
+ return backendUrl.replace(/\/+$/, "");
16
+ }
17
+ async function callAuthJson(base, method, path, opts) {
18
+ const url = new URL(path.startsWith("/") ? path.slice(1) : path, `${base}/`);
19
+ if (opts?.query) {
20
+ for (const [k, v] of Object.entries(opts.query)) {
21
+ if (v != null && v !== "")
22
+ url.searchParams.set(k, v);
23
+ }
24
+ }
25
+ const headers = { Accept: "application/json" };
26
+ const hasBody = opts?.json != null && (method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE");
27
+ if (hasBody) {
28
+ headers["Content-Type"] = "application/json";
29
+ }
30
+ if (opts?.token) {
31
+ headers.Authorization = `Bearer ${opts.token.trim()}`;
32
+ }
33
+ const init = { method, headers };
34
+ if (hasBody && opts?.json) {
35
+ init.body = JSON.stringify(opts.json);
36
+ }
37
+ const resp = await fetch(url, init);
38
+ const text = await resp.text();
39
+ let parsed;
40
+ try {
41
+ parsed = text ? JSON.parse(text) : {};
42
+ }
43
+ catch {
44
+ parsed = { raw: text };
45
+ }
46
+ const body = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
47
+ ? parsed
48
+ : { value: parsed };
49
+ return {
50
+ status_code: resp.status,
51
+ ok: resp.ok,
52
+ ...body,
53
+ };
54
+ }
55
+ /**
56
+ * Register MCP tools for routes/auth (email, OTP, password, JWT).
57
+ * Proxies to the same process as opentest-local (OPENTEST_BACKEND, e.g. http://127.0.0.1:8000).
58
+ */
59
+ export function registerUserAuthFlowTools(server, backendUrl) {
60
+ const base = normalizeBase(backendUrl);
61
+ server.tool("user_auth_signup", "Register with email and password. Sends an OTP; next call user_auth_verify_otp. Uses POST /auth/signup on OPENTEST_BACKEND (no API key).", {
62
+ email: z.string().describe("User email"),
63
+ password: z.string().describe("Password (min 6 characters on the server)"),
64
+ }, async (args) => {
65
+ try {
66
+ const r = await callAuthJson(base, "POST", "/auth/signup", {
67
+ json: { email: args.email, password: args.password },
68
+ });
69
+ if (!r.ok) {
70
+ return toolError(String(r.detail ?? r.message ?? "signup failed"), r.status_code === 400 ? "invalid_input" : "backend_error", {
71
+ next_actions: ["If email exists, use user_auth_login or user_auth_resend_otp."],
72
+ related_tools: ["user_auth_verify_otp", "user_auth_login"],
73
+ });
74
+ }
75
+ return jsonOk(r);
76
+ }
77
+ catch (e) {
78
+ const m = e instanceof Error ? e.message : String(e);
79
+ return toolError(m, "backend_error", {
80
+ next_actions: ["Ensure OPENTEST_BACKEND is running and JWT_SECRET is set on the server."],
81
+ });
82
+ }
83
+ });
84
+ server.tool("user_auth_verify_otp", "Verify the email OTP and receive access_token (JWT). POST /auth/verify-otp.", { email: z.string(), otp: z.string().describe("6-digit code from email") }, async (args) => {
85
+ try {
86
+ const r = await callAuthJson(base, "POST", "/auth/verify-otp", {
87
+ json: { email: args.email, otp: args.otp },
88
+ });
89
+ if (!r.ok) {
90
+ return toolError(String(r.detail ?? "verify failed"), "auth_required", {
91
+ next_actions: ["Request a new code with user_auth_resend_otp if expired."],
92
+ related_tools: ["user_auth_resend_otp", "user_auth_signup"],
93
+ });
94
+ }
95
+ return jsonOk(r);
96
+ }
97
+ catch (e) {
98
+ return toolError(e instanceof Error ? e.message : String(e), "backend_error");
99
+ }
100
+ });
101
+ server.tool("user_auth_login", "Login with email and password. Returns access_token if email is verified. POST /auth/login.", { email: z.string(), password: z.string() }, async (args) => {
102
+ try {
103
+ const r = await callAuthJson(base, "POST", "/auth/login", {
104
+ json: { email: args.email, password: args.password },
105
+ });
106
+ if (!r.ok) {
107
+ const sc = r.status_code;
108
+ const code = sc === 403 || sc === 401 ? "auth_required" : "backend_error";
109
+ return toolError(String(r.detail ?? "login failed"), code, {
110
+ next_actions: [
111
+ "If 403 email not verified: use user_auth_verify_otp or user_auth_resend_otp.",
112
+ "If 403 migrated account: use user_auth_forgot_password or user_auth_set_password flow.",
113
+ ],
114
+ related_tools: ["user_auth_verify_otp", "user_auth_needs_password", "user_auth_set_password"],
115
+ });
116
+ }
117
+ return jsonOk(r);
118
+ }
119
+ catch (e) {
120
+ return toolError(e instanceof Error ? e.message : String(e), "backend_error");
121
+ }
122
+ });
123
+ server.tool("user_auth_needs_password", "Check if an account must set a password (e.g. migrated user). GET /auth/needs-password?email=", { email: z.string().optional().describe("Email to check; omit to return false/false from server") }, async (args) => {
124
+ try {
125
+ const r = await callAuthJson(base, "GET", "/auth/needs-password", {
126
+ query: { email: args.email },
127
+ });
128
+ if (!r.ok) {
129
+ return toolError(String(r.detail ?? "request failed"), "backend_error");
130
+ }
131
+ return jsonOk(r);
132
+ }
133
+ catch (e) {
134
+ return toolError(e instanceof Error ? e.message : String(e), "backend_error");
135
+ }
136
+ });
137
+ server.tool("user_auth_resend_otp", "Resend verification OTP. POST /auth/resend-otp with { email }.", { email: z.string() }, async (args) => {
138
+ try {
139
+ const r = await callAuthJson(base, "POST", "/auth/resend-otp", {
140
+ json: { email: args.email },
141
+ });
142
+ if (!r.ok) {
143
+ return toolError(String(r.detail ?? "resend failed"), "invalid_input");
144
+ }
145
+ return jsonOk(r);
146
+ }
147
+ catch (e) {
148
+ return toolError(e instanceof Error ? e.message : String(e), "backend_error");
149
+ }
150
+ });
151
+ server.tool("user_auth_forgot_password", "Request password reset OTP. POST /auth/forgot-password with { email }.", { email: z.string() }, async (args) => {
152
+ try {
153
+ const r = await callAuthJson(base, "POST", "/auth/forgot-password", {
154
+ json: { email: args.email },
155
+ });
156
+ if (!r.ok) {
157
+ return toolError(String(r.detail ?? "forgot failed"), "backend_error");
158
+ }
159
+ return jsonOk(r);
160
+ }
161
+ catch (e) {
162
+ return toolError(e instanceof Error ? e.message : String(e), "backend_error");
163
+ }
164
+ });
165
+ server.tool("user_auth_set_password", "Set a new password using OTP (migrated / forgot flow). Returns access_token. POST /auth/set-password.", {
166
+ email: z.string(),
167
+ otp: z.string(),
168
+ new_password: z.string().min(6),
169
+ }, async (args) => {
170
+ try {
171
+ const r = await callAuthJson(base, "POST", "/auth/set-password", {
172
+ json: {
173
+ email: args.email,
174
+ otp: args.otp,
175
+ new_password: args.new_password,
176
+ },
177
+ });
178
+ if (!r.ok) {
179
+ return toolError(String(r.detail ?? "set password failed"), "auth_required", {
180
+ next_actions: ["Request a new OTP with user_auth_forgot_password or user_auth_resend_otp."],
181
+ related_tools: ["user_auth_forgot_password"],
182
+ });
183
+ }
184
+ return jsonOk(r);
185
+ }
186
+ catch (e) {
187
+ return toolError(e instanceof Error ? e.message : String(e), "backend_error");
188
+ }
189
+ });
190
+ server.tool("user_auth_me", "Current user profile (JWT). GET /auth/me with Authorization: Bearer access_token from login/verify.", {
191
+ access_token: z.string().describe("JWT from user_auth_login or user_auth_verify_otp"),
192
+ }, async (args) => {
193
+ try {
194
+ const r = await callAuthJson(base, "GET", "/auth/me", {
195
+ token: args.access_token,
196
+ });
197
+ if (!r.ok) {
198
+ return toolError(String(r.detail ?? "unauthorized"), "auth_required", {
199
+ next_actions: ["Call user_auth_login to obtain a fresh access_token."],
200
+ related_tools: ["user_auth_login"],
201
+ });
202
+ }
203
+ return jsonOk(r);
204
+ }
205
+ catch (e) {
206
+ return toolError(e instanceof Error ? e.message : String(e), "backend_error");
207
+ }
208
+ });
209
+ server.tool("user_auth_change_password", "Change password for the signed-in user. POST /auth/change-password with JWT.", {
210
+ access_token: z.string(),
211
+ current_password: z.string(),
212
+ new_password: z.string().min(6),
213
+ }, async (args) => {
214
+ try {
215
+ const r = await callAuthJson(base, "POST", "/auth/change-password", {
216
+ token: args.access_token,
217
+ json: {
218
+ current_password: args.current_password,
219
+ new_password: args.new_password,
220
+ },
221
+ });
222
+ if (!r.ok) {
223
+ return toolError(String(r.detail ?? "change password failed"), r.status_code === 401 ? "auth_required" : "invalid_input", {
224
+ related_tools: ["user_auth_login", "user_auth_me"],
225
+ });
226
+ }
227
+ return jsonOk(r);
228
+ }
229
+ catch (e) {
230
+ return toolError(e instanceof Error ? e.message : String(e), "backend_error");
231
+ }
232
+ });
233
+ server.tool("user_auth_delete_account", "Delete the current account. DELETE /auth/me with JWT. Irreversible.", {
234
+ access_token: z.string().describe("JWT from user_auth_login or user_auth_verify_otp"),
235
+ }, async (args) => {
236
+ try {
237
+ const r = await callAuthJson(base, "DELETE", "/auth/me", {
238
+ token: args.access_token,
239
+ });
240
+ if (!r.ok) {
241
+ return toolError(String(r.detail ?? "delete failed"), "auth_required", {
242
+ related_tools: ["user_auth_login"],
243
+ });
244
+ }
245
+ return jsonOk(r);
246
+ }
247
+ catch (e) {
248
+ return toolError(e instanceof Error ? e.message : String(e), "backend_error");
249
+ }
250
+ });
251
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "opentestmcp",
3
+ "version": "0.3.0",
4
+ "description": "AI QA engineer for your IDE — test UI flows and API endpoints with inline visual results",
5
+ "type": "module",
6
+ "bin": {
7
+ "opentestmcp": "bin/cli.js",
8
+ "opentest-mcp": "bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc -p tsconfig.server.json",
12
+ "dev": "tsx src/server.ts",
13
+ "open-mcp-preview": "node scripts/emit-mcp-preview.mjs",
14
+ "open-mcp-preview-in-cursor": "node scripts/open-cursor-preview.mjs"
15
+ },
16
+ "dependencies": {
17
+ "@modelcontextprotocol/ext-apps": "^0.1.0",
18
+ "@modelcontextprotocol/sdk": "^1.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^25.5.0",
22
+ "tsx": "^4.0.0",
23
+ "typescript": "^5.8.0"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "testing",
28
+ "qa",
29
+ "mcp-apps",
30
+ "playwright",
31
+ "browser-automation",
32
+ "api-testing",
33
+ "cursor",
34
+ "claude-code"
35
+ ],
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/anthropics/opentest-mcp.git"
40
+ },
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "files": [
45
+ "dist/",
46
+ "bin/",
47
+ "browser/",
48
+ "README.md",
49
+ "LICENSE"
50
+ ]
51
+ }